diff options
author | Paul Gilbert | 2019-10-13 15:59:20 -0700 |
---|---|---|
committer | Paul Gilbert | 2019-10-26 11:13:29 -0700 |
commit | d8048720fc6c1104368c995022285462cf9f32f9 (patch) | |
tree | 926394805cb957b66b8431d6d5af5fed7a91ab8d /engines/glk/level9 | |
parent | b8be17fd02fe32d6cea363cc302014090fba4348 (diff) | |
download | scummvm-rg350-d8048720fc6c1104368c995022285462cf9f32f9.tar.gz scummvm-rg350-d8048720fc6c1104368c995022285462cf9f32f9.tar.bz2 scummvm-rg350-d8048720fc6c1104368c995022285462cf9f32f9.zip |
GLK: LEVEL9: Added subengine files
Diffstat (limited to 'engines/glk/level9')
-rw-r--r-- | engines/glk/level9/bitmap.cpp | 1460 | ||||
-rw-r--r-- | engines/glk/level9/level9_main.cpp | 3599 | ||||
-rw-r--r-- | engines/glk/level9/level9_main.h | 123 | ||||
-rw-r--r-- | engines/glk/level9/os_glk.cpp | 6084 |
4 files changed, 11266 insertions, 0 deletions
diff --git a/engines/glk/level9/bitmap.cpp b/engines/glk/level9/bitmap.cpp new file mode 100644 index 0000000000..976c8a1a8e --- /dev/null +++ b/engines/glk/level9/bitmap.cpp @@ -0,0 +1,1460 @@ +/* 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/level9/level9_main.h" +#include "common/file.h" + +namespace Glk { +namespace Level9 { + +extern Bitmap *bitmap; + +void L9Allocate(L9BYTE **ptr, L9UINT32 Size); + +L9BOOL bitmap_exists(char *file) { + return Common::File::exists(file); +} + +L9BYTE *bitmap_load(char *file, L9UINT32 *size) { + L9BYTE *data = NULL; + + Common::File f; + if (f.open(file)) { + *size = f.size(); + L9Allocate(&data, *size); + f.read(data, *size); + + f.close(); + } + return data; +} + +Bitmap *bitmap_alloc(int x, int y) { + Bitmap *b = NULL; + L9Allocate((L9BYTE **)&b, sizeof(Bitmap) + (x * y)); + + b->width = x; + b->height = y; + b->bitmap = ((L9BYTE *)b) + sizeof(Bitmap); + b->npalette = 0; + return b; +} + +/* + A PC or ST palette colour is a sixteen bit value in which the low three nybbles + hold the rgb colour values. The lowest nybble holds the blue value, the + second nybble the blue value and the third nybble the red value. (The high + nybble is ignored). Within each nybble, only the low three bits are used + IE the value can only be 0-7 not the full possible 0-15 and so the MSbit in + each nybble is always 0. +*/ +Colour bitmap_pcst_colour(int big, int small) { + Colour col; + L9UINT32 r = big & 0xF; + L9UINT32 g = (small >> 4) & 0xF; + L9UINT32 b = small & 0xF; + + r *= 0x49; + r >>= 1; + g *= 0x49; + g >>= 1; + b *= 0x49; + b >>= 1; + + col.red = (L9BYTE)(r & 0xFF); + col.green = (L9BYTE)(g & 0xFF); + col.blue = (L9BYTE)(b & 0xFF); + return col; +} + +/* + ST Bitmaps + + On the ST different graphics file formats were used for the early V4 + games (Knight Orc, Gnome Ranger) and the later V4 games (Lancelot, + Ingrid's Back, Time & Magik and Scapeghost). +*/ + +/* + Extracts the number of pixels requested from an eight-byte data block (4 bit- + planes) passed to it. + + Note: On entry each one of four pointers is set to point to the start of each + bit-plane in the block. The function then indexes through each byte in + each bit plane. and uses shift and mask operations to extract each four + bit pixel into an L9PIXEL. + + The bit belonging to the pixel in the current byte of the current bit- + plane is moved to its position in an eight-bit pixel. The byte is then + masked by a value to select only that bit and added to the final pixel + value. +*/ +L9UINT32 bitmap_st1_decode_pixels(L9BYTE *pic, L9BYTE *data, L9UINT32 count, L9UINT32 pixels) { + L9UINT32 bitplane_length = count / 4; /* length of each bitplane */ + L9BYTE *bitplane0 = data; /* address of bit0 bitplane */ + L9BYTE *bitplane1 = data + (bitplane_length); /* address of bit1 bitplane */ + L9BYTE *bitplane2 = data + (bitplane_length * 2); /* address of bit2 bitplane */ + L9BYTE *bitplane3 = data + (bitplane_length * 3); /* address of bit3 bitplane */ + L9UINT32 bitplane_index, pixel_index = 0; /* index variables */ + + for (bitplane_index = 0; bitplane_index < bitplane_length; bitplane_index++) { + /* build the eight pixels from the current bitplane bytes, high bit to low */ + + /* bit7 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] >> 4) & 0x08) + + ((bitplane2[bitplane_index] >> 5) & 0x04) + + ((bitplane1[bitplane_index] >> 6) & 0x02) + + ((bitplane0[bitplane_index] >> 7) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit6 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] >> 3) & 0x08) + + ((bitplane2[bitplane_index] >> 4) & 0x04) + + ((bitplane1[bitplane_index] >> 5) & 0x02) + + ((bitplane0[bitplane_index] >> 6) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit5 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] >> 2) & 0x08) + + ((bitplane2[bitplane_index] >> 3) & 0x04) + + ((bitplane1[bitplane_index] >> 4) & 0x02) + + ((bitplane0[bitplane_index] >> 5) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit4 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] >> 1) & 0x08) + + ((bitplane2[bitplane_index] >> 2) & 0x04) + + ((bitplane1[bitplane_index] >> 3) & 0x02) + + ((bitplane0[bitplane_index] >> 4) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit3 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index]) & 0x08) + + ((bitplane2[bitplane_index] >> 1) & 0x04) + + ((bitplane1[bitplane_index] >> 2) & 0x02) + + ((bitplane0[bitplane_index] >> 3) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit2 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] << 1) & 0x08) + + ((bitplane2[bitplane_index]) & 0x04) + + ((bitplane1[bitplane_index] >> 1) & 0x02) + + ((bitplane0[bitplane_index] >> 2) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit1 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] << 2) & 0x08) + + ((bitplane2[bitplane_index] << 1) & 0x04) + + ((bitplane1[bitplane_index]) & 0x02) + + ((bitplane0[bitplane_index] >> 1) & 0x01); + if (pixels == ++pixel_index) + break; + + /* bit0 byte */ + pic[pixel_index] = ((bitplane3[bitplane_index] << 3) & 0x08) + + ((bitplane2[bitplane_index] << 2) & 0x04) + + ((bitplane1[bitplane_index] << 1) & 0x02) + + ((bitplane0[bitplane_index]) & 0x01); + if (pixels == ++pixel_index) + break; + } + + return pixel_index; +} + +/* + The ST image file has the following format. It consists of a 44 byte header + followed by the image data. + + The header has the following format: + Bytes 0-31: sixteen entry ST palette + Bytes 32-33: padding + Bytes 34-35: big-endian word holding number of bitplanes needed to make + up a row of pixels* + Bytes 36-37: padding + Bytes 38-39: big-endian word holding number of rows in the image* + Bytes 40-41: padding** + Bytes 42-43: mask for pixels to show in last 16 pixel block. Again, this + is big endian + + [*] these are probably big-endian unsigned longs but I have designated + the upper two bytes as padding because (a) Level 9 does not need + them as longs and (b) using unsigned shorts reduces byte sex induced + byte order juggling. + [**] not certain what this is for but I suspect that, like bytes 42-43 + it is a mask to indicate which pixels to show, in this case in the + first 16 pixel block + + The image data is essentially a memory dump of the video RAM representing + the image in lo-res mode. In lo-res mode each row is 320 pixels wide + and each pixel can be any one of sixteen colours - needs 4 bits to store. + + In the ST video memory (in lo-res mode which we are dealing with here) + is organised as follows. The lowest point in memory in the frame buffer + represents the top-left of the screen, the highest the bottom-right. + Each row of pixels is stored in sequence. + + Within each pixel row the pixels are stored as follows. Each row is + divided into groups of 16 pixels. Each sixteen pixel group is stored + in 8 bytes, logically four groups of two. Each two byte pair + is a bit-plane for that sixteen pixel group - that is it stores the + same bit of each pixel in that group. The first two bytes store the + lowest bit, the second pair the second bit &c. + + The word at bytes 34-35 of the header stores the number of bitplanes + that make up each pixel row in the image. Multplying this number by + four gives the number of pixels in the row***. For title and frame + images that will be 320, for sub-images it will be less. + + [***] Not always exactly. For GnomeRanger sub-images this value is 60 + - implying there are 240 pixels per row. In fact there are only + 225 pixels in each row. To identify this situation look at the + big-endian word in bytes 42-43 of the header. This is a mask + telling you the pixels to use. Each bit represents one pixel in + the block, with the MSBit representing the first pixel and the + LSbit the last. + + In this situation, the file does contain the entire sixteen + pixel block (it has to with the bitplane arrangement) but + the pixels which are not part of the image are just noise. When + decoding the image, the L9BITMAP produced has the actual pixel + dimensions - the surplus pixels are discarded. + + I suspect, though I have not found an instance, that in theory + the same situation could apply at the start of a pixel row and that + in this case the big-endian word at bytes 40-41 is the mask. + + Having obtained the pixel dimensions of the image the function uses + them to allocate memory for the bitmap and then extracts the pixel + information from the bitmap row by row. For each row eight byte blocks + are read from the image data and passed to UnpackSTv1Pixels along with + the number of pixels to extract (usually 16, possibly less for the last + block in a row.) +*/ +L9BOOL bitmap_st1_decode(char *file, int x, int y) { + L9BYTE *data = NULL; + int i, xi, yi, max_x, max_y, last_block; + int bitplanes_row, bitmaps_row, pixel_count, get_pixels; + + L9UINT32 size; + data = bitmap_load(file, &size); + if (data == NULL) + return FALSE; + + bitplanes_row = data[35] + data[34] * 256; + bitmaps_row = bitplanes_row / 4; + max_x = bitplanes_row * 4; + max_y = data[39] + data[38] * 256; + last_block = data[43] + data[42] * 256; + + /* Check if sub-image with rows shorter than max_x */ + if (last_block != 0xFFFF) { + /* use last_block to adjust max_x */ + i = 0; + while ((0x0001 & last_block) == 0) { /* test for ls bit set */ + last_block >>= 1; /* if not, shift right one bit */ + i++; + } + max_x = max_x - i; + } + + if (max_x > MAX_BITMAP_WIDTH || max_y > MAX_BITMAP_HEIGHT) { + free(data); + return FALSE; + } + + if ((x == 0) && (y == 0)) { + if (bitmap) + free(bitmap); + bitmap = bitmap_alloc(max_x, max_y); + } + if (bitmap == NULL) { + free(data); + return FALSE; + } + + if (x + max_x > bitmap->width) + max_x = bitmap->width - x; + if (y + max_y > bitmap->height) + max_y = bitmap->height - y; + + for (yi = 0; yi < max_y; yi++) { + pixel_count = 0; + for (xi = 0; xi < bitmaps_row; xi++) { + if ((max_x - pixel_count) < 16) + get_pixels = max_x - pixel_count; + else + get_pixels = 16; + + pixel_count += bitmap_st1_decode_pixels( + bitmap->bitmap + ((y + yi) * bitmap->width) + x + (xi * 16), + data + 44 + (yi * bitplanes_row * 2) + (xi * 8), 8, get_pixels); + } + } + + bitmap->npalette = 16; + for (i = 0; i < 16; i++) + bitmap->palette[i] = bitmap_pcst_colour(data[(i * 2)], data[1 + (i * 2)]); + + free(data); + return TRUE; +} + +void bitmap_st2_name(int num, char *dir, char *out) { + /* title picture is #30 */ + if (num == 0) + num = 30; + sprintf(out, "%s%d.squ", dir, num); +} + +/* + PC Bitmaps + + On the PC different graphics file formats were used for the early V4 + games (Knight Orc, Gnome Ranger) and the later V4 games (Lancelot, + Ingrid's Back, Time & Magik and Scapeghost). + + The ST and the PC both use the same image file format for the later + V4 games (Lancelot, Ingrid's Back, Time & Magik and Scapeghost.) +*/ + +void bitmap_pc_name(int num, char *dir, char *out) { + /* title picture is #30 */ + if (num == 0) + num = 30; + sprintf(out, "%s%d.pic", dir, num); +} + +/* + The EGA standard for the IBM PCs and compatibles defines 64 colors, any + 16 of which can be mapped to the usable palette at any given time. If + you display these 64 colors in numerical order, 16 at a time, you get a + hodgepodge of colors in no logical order. The 64 EGA color numbers are + assigned in a way that the numbers can easily be converted to a relative + intensity of each of the three phosphor colors R,G,B. If the number is + converted to six bit binary, the most significant three bits represent + the 25% level of R,G,B in that order and the least significant three + bits represent the 75% level of R,G,B in that order. Take EGA color 53 + for example. In binary, 53 is 110101. Since both R bits are on, R = 1.0. + Of the G bits only the 25% bit is on so G = 0.25. Of the B bits only the + 75% bit is on so B = 0.75. +*/ +Colour bitmap_pc1_colour(int i) { + Colour col; + col.red = (((i & 4) >> 1) | ((i & 0x20) >> 5)) * 0x55; + col.green = ((i & 2) | ((i & 0x10) >> 4)) * 0x55; + col.blue = (((i & 1) << 1) | ((i & 8) >> 3)) * 0x55; + return col; +} + +/* + The PC (v1) image file has the following format. It consists of a 22 + byte header organised like this: + + Byte 0: probably a file type flag + Byte 1: the MSB of the file's length as a word + Bytes 2-3: little-endian word with picture width in pixels + Bytes 4-5: little-endian word with picture height in pixel rows + Bytes 6-21: the image colour table. One EGA colour in each byte + + The image data is extremely simple. The entire block is packed array + of 4-bit pixels - IE each byte holds two pixels - the first in the high + nybble, the second in the low. The pixel value is an index into the + image colour table. The pixels are organised with the top left first and + bottom left last, each row in turn. +*/ +L9BOOL bitmap_pc1_decode(char *file, int x, int y) { + L9BYTE *data = NULL; + int i, xi, yi, max_x, max_y; + + L9UINT32 size; + data = bitmap_load(file, &size); + if (data == NULL) + return FALSE; + + max_x = data[2] + data[3] * 256; + max_y = data[4] + data[5] * 256; + if (max_x > MAX_BITMAP_WIDTH || max_y > MAX_BITMAP_HEIGHT) { + free(data); + return FALSE; + } + + if ((x == 0) && (y == 0)) { + if (bitmap) + free(bitmap); + bitmap = bitmap_alloc(max_x, max_y); + } + if (bitmap == NULL) { + free(data); + return FALSE; + } + + if (x + max_x > bitmap->width) + max_x = bitmap->width - x; + if (y + max_y > bitmap->height) + max_y = bitmap->height - y; + + for (yi = 0; yi < max_y; yi++) { + for (xi = 0; xi < max_x; xi++) { + bitmap->bitmap[(bitmap->width * (y + yi)) + (x + xi)] = + (data[23 + ((yi * max_x) / 2) + (xi / 2)] >> ((1 - (xi & 1)) * 4)) & 0x0f; + } + } + + bitmap->npalette = 16; + for (i = 0; i < 16; i++) + bitmap->palette[i] = bitmap_pc1_colour(data[6 + i]); + + free(data); + return TRUE; +} + +/* + The PC (v2) image file has the following format. It consists of a 44 + byte header followed by the image data. + + The header has the following format: + Bytes 0-1: "datalen": length of file -1 as a big-endian word* + Bytes 2-3: "flagbyte1 & flagbyte2": unknown, possibly type identifiers. + Usually 0xFF or 0xFE followed by 0x84, 0x72, 0xFF, 0xFE or + some other (of a fairly small range of possibles) byte. + Bytes 4-35: "colour_index[]": sixteen entry palette. Basically an ST + palette (even if in a PC image file. Each entry is a sixteen + bit value in which the low three nybbles hold the rgb colour + values. The lowest nybble holds the blue value, the second + nybble the blue value and the third nybble the red value. (The + high nybble is ignored). Within each nybble, only the low + three bits are used IE the value can only be 0-7 not the full + possible 0-15 and so the MSbit in each nybble is always 0.**, + Bytes 36-37: "width": image width in pixels as a big-endian word + Bytes 38-39: "numrows": image height in pixel rows as a big-endian word + Byte 40: "seedByte": seed byte to start picture decoding. + Byte 41: "padByte": unknown. Possibly padding to word align the next + element? + Bytes 42-297: "pixelTable": an array of 0x100 bytes used as a lookup table + for pixel values + Bytes 298-313: "bitStripTable": an array of 0x10 bytes used as a lookup table + for the number of bytes to strip from the bit stream for the pixel being + decoded + Bytes 314-569: "indexByteTable": an array of 0x100 bytes used as a lookup + table to index into bitStripTable and pixelTable**** + + The encoded image data then follows ending in a 0x00 at the file length stored + in the first two bytes of the file. there is then one extra byte holding a + checksum produced by the addition of all the bytes in the file (except the first + two and itself)* + + [*] in some PC games the file is padded out beyond this length to the + nearest 0x80/0x00 boundary with the byte 0x1A. The valid data in the + file still finishes where this word says with the checkbyte following it. + [**] I imagine when a game was running on a PC this standard palette + was algorithimcally changed to suit the graphics mode being used + (Hercules, MDA, CGA, EGA, MCGA, VGA &c.) + [***] Note also, in image 1 of PC Time & Magik I think one palette entry + is bad as what should be white in the image is actually set to + a very pale yellow. This is corrected with the display of the next + sub-picture and I am pretty sure it is note a decoding problem + here as when run on the PC the same image has the same pale yellow + cast. + [****] for detail of how all this works see below + + As this file format is intended for two very different platforms the decoded + imaged data is in a neutral, intermediate form. Each pixel is extracted as a + byte with only the low four bits significant. The pixel value is an index into + the sixteen entry palette. + + The pixel data is compressed, presumably to allow a greater number of images + to be distributed on the (rather small) default ST & PC floppy disks (in both + cases about 370 Kbytes.)***** + + Here's how to decode the data. The image data is actually a contiguous bit + stream with the byte structure on disk having almost no relevance to the + encoding. We access the bit stream via a two-byte buffer arranged as a word. + + Preparation: + + Initially, move the first byte from the image data into the low byte of + theBitStreamBuffer and move the second byte of the image data into the + high byte of theBitStreamBuffer. + + Set a counter (theBufferBitCounter) to 8 which you will use to keep track + of when it is necesary to refill the buffer. + + Set a L9BYTE variable (theNewPixel) to byte 40 (seedByte) of the header. + We need to do this because as part of identifying the pixel being + extracted we need to know the value of the previous pixel extracted. Since + none exists at this point we must prime this variable with the correct + value. + + Extraction: + + Set up a loop which you will execute once for each pixel to be extracted + and within that loop do as follows. + + Copy the low byte of theBitStreamBuffer to an L9BYTE + (theNewPixelIndexSelector). Examine theNewPixelIndexSelector. If this + is 0xFF this flags that the index to the new pixel is present as a + literal in the bit stream; if it is NOT 0xFF then the new pixel index + value has to be decoded. + + If theNewPixelIndexSelector is NOT 0xFF do as follows: + + Set the variable theNewPixelIndex to the byte in the + indexByteTable array of the header indexed by + theNewPixelIndexSelector. + + Set the variable theBufferBitStripCount to the value in the + bitStripTable array of the header indexed by theNewPixelIndex. + + One-by-one use right bit shift (>>) to remove + theBufferBitStripCount bits from theBitStreamBuffer. After each + shift decrement theBufferBitCounter and check whether it has + reached 0. If it has, get the next byte from the image data and + insert it in the high byte of theBitStreamBuffer and reset + theBufferBitCounter to 8. What is happening here is as we remove + each bit from the bottom of the bit stream buffer we check to see + if there are any bits left in the high byte of the buffer. As soon + as we know there are none, we refill it with the next eight bits + from the image data. + + When this 'bit-stripping' is finished, other than actually identifying + the new pixel we are nearly done. I will leave that for the moment and + look at what happens if the low byte of theBitStreamBuffer we put in + theNewPixelIndexSelector was actually 0xFF: + + In this case, instead of the above routine we begin by removing + the low eight bits from the theBitStreamBuffer. We use the same + ono-by-one bit shift right process described above to do this, + again checking after each shift if it is necesary to refill the + buffer's high byte. + + When the eight bits have been removed we set theNewPixelIndex to + the value of the low four bits of theBitStreamBuffer. Having done + that we again one-by-one strip off those low four bits from the + theBitStreamBuffer, again checking if we need to refill the buffer + high byte. + + Irrespective of whether we initially had 0xFF in + theNewPixelIndexSelector we now have a new value in theNewPixelIndex. + This value is used as follows to obtain the new pixel value. + + The variable theNewPixel contains either the seedByte or the value of + the previously extracted pixel. In either case this is a 4-bit value + in the lower 4 bits. Use the left bit shift operator (or multiply by + 16) to shift those four bits into the high four bits of theNewPixel. + + Add the value in theNewPixelIndex (it is a 4-bit value) to + theNewPixel. The resulting value is used as an index into the + pixelTable array of the header to get the actual new pixel value so + theNewPixel = header.pixelTable[theNewPixel] gets us our new pixel and + primes theNewPixel for the same process next time around the loop. + + Having got our new pixel it is stored in the next empty space in the + bitmap and we loop back and start again. + + [*****] I am not sure how the compression was done - someone with a better + understanding of this area may be able to work out the method from the above. + I worked out how to decode it by spending many, many hours tracing through the + code in a debugger - thanks to the now defunct HiSoft for their DevPac ST and + Gerin Philippe for NoSTalgia <http://users.skynet.be/sky39147/>. +*/ +L9BOOL bitmap_pc2_decode(char *file, int x, int y) { + L9BYTE *data = NULL; + int i, xi, yi, max_x, max_y; + + L9BYTE theNewPixel, theNewPixelIndex; + L9BYTE theBufferBitCounter, theNewPixelIndexSelector, theBufferBitStripCount; + L9UINT16 theBitStreamBuffer, theImageDataIndex; + L9BYTE *theImageFileData; + + L9UINT32 size; + data = bitmap_load(file, &size); + if (data == NULL) + return FALSE; + + max_x = data[37] + data[36] * 256; + max_y = data[39] + data[38] * 256; + if (max_x > MAX_BITMAP_WIDTH || max_y > MAX_BITMAP_HEIGHT) { + free(data); + return FALSE; + } + + if ((x == 0) && (y == 0)) { + if (bitmap) + free(bitmap); + bitmap = bitmap_alloc(max_x, max_y); + } + if (bitmap == NULL) { + free(data); + return FALSE; + } + + if (x + max_x > bitmap->width) + max_x = bitmap->width - x; + if (y + max_y > bitmap->height) + max_y = bitmap->height - y; + + /* prime the new pixel variable with the seed byte */ + theNewPixel = data[40]; + /* initialise the index to the image data */ + theImageDataIndex = 0; + /* prime the bit stream buffer */ + theImageFileData = data + 570; + theBitStreamBuffer = theImageFileData[theImageDataIndex++]; + theBitStreamBuffer = theBitStreamBuffer + + (0x100 * theImageFileData[theImageDataIndex++]); + /* initialise the bit stream buffer bit counter */ + theBufferBitCounter = 8; + + for (yi = 0; yi < max_y; yi++) { + for (xi = 0; xi < max_x; xi++) { + theNewPixelIndexSelector = (theBitStreamBuffer & 0x00FF); + if (theNewPixelIndexSelector != 0xFF) { + /* get index for new pixel and bit strip count */ + theNewPixelIndex = (data + 314)[theNewPixelIndexSelector]; + /* get the bit strip count */ + theBufferBitStripCount = (data + 298)[theNewPixelIndex]; + /* strip theBufferBitStripCount bits from theBitStreamBuffer */ + while (theBufferBitStripCount > 0) { + theBitStreamBuffer = theBitStreamBuffer >> 1; + theBufferBitStripCount--; + theBufferBitCounter--; + if (theBufferBitCounter == 0) { + /* need to refill the theBitStreamBuffer high byte */ + theBitStreamBuffer = theBitStreamBuffer + + (0x100 * theImageFileData[theImageDataIndex++]); + /* re-initialise the bit stream buffer bit counter */ + theBufferBitCounter = 8; + } + } + } else { + /* strip the 8 bits holding 0xFF from theBitStreamBuffer */ + theBufferBitStripCount = 8; + while (theBufferBitStripCount > 0) { + theBitStreamBuffer = theBitStreamBuffer >> 1; + theBufferBitStripCount--; + theBufferBitCounter--; + if (theBufferBitCounter == 0) { + /* need to refill the theBitStreamBuffer high byte */ + theBitStreamBuffer = theBitStreamBuffer + + (0x100 * theImageFileData[theImageDataIndex++]); + /* re-initialise the bit stream buffer bit counter */ + theBufferBitCounter = 8; + } + } + /* get the literal pixel index value from the bit stream */ + theNewPixelIndex = (0x000F & theBitStreamBuffer); + theBufferBitStripCount = 4; + /* strip 4 bits from theBitStreamBuffer */ + while (theBufferBitStripCount > 0) { + theBitStreamBuffer = theBitStreamBuffer >> 1; + theBufferBitStripCount--; + theBufferBitCounter--; + if (theBufferBitCounter == 0) { + /* need to refill the theBitStreamBuffer high byte */ + theBitStreamBuffer = theBitStreamBuffer + + (0x100 * theImageFileData[theImageDataIndex++]); + /* re-initialise the bit stream buffer bit counter */ + theBufferBitCounter = 8; + } + } + } + + /* shift the previous pixel into the high four bits of theNewPixel */ + theNewPixel = (0xF0 & (theNewPixel << 4)); + /* add the index to the new pixel to theNewPixel */ + theNewPixel = theNewPixel + theNewPixelIndex; + /* extract the nex pixel from the table */ + theNewPixel = (data + 42)[theNewPixel]; + /* store new pixel in the bitmap */ + bitmap->bitmap[(bitmap->width * (y + yi)) + (x + xi)] = theNewPixel; + } + } + + bitmap->npalette = 16; + for (i = 0; i < 16; i++) + bitmap->palette[i] = bitmap_pcst_colour(data[4 + (i * 2)], data[5 + (i * 2)]); + + free(data); + return TRUE; +} + +BitmapType bitmap_pc_type(char *file) { + BitmapType type = PC2_BITMAPS; + + Common::File f; + if (f.open(file)) { + L9BYTE data[6]; + int x, y; + + if (f.read(data, sizeof(data)) != sizeof(data) && !f.eos()) + return NO_BITMAPS; + f.close(); + + x = data[2] + data[3] * 256; + y = data[4] + data[5] * 256; + + if ((x == 0x0140) && (y == 0x0087)) + type = PC1_BITMAPS; + if ((x == 0x00E0) && (y == 0x0074)) + type = PC1_BITMAPS; + if ((x == 0x0140) && (y == 0x0087)) + type = PC1_BITMAPS; + if ((x == 0x00E1) && (y == 0x0076)) + type = PC1_BITMAPS; + } + + return type; +} + +/* + Amiga Bitmaps +*/ + +void bitmap_noext_name(int num, char *dir, char *out) { + if (num == 0) { + sprintf(out, "%stitle", dir); + if (Common::File::exists(out)) + return; + + num = 30; + } + + sprintf(out, "%s%d", dir, num); +} + +int bitmap_amiga_intensity(int col) { + return (int)(pow((double)col / 15, 1.0 / 0.8) * 0xff); +} + +/* + Amiga palette colours are word length structures with the red, green and blue + values stored in the second, third and lowest nybles respectively. The high + nybble is always zero. +*/ +Colour bitmap_amiga_colour(int i1, int i2) { + Colour col; + col.red = bitmap_amiga_intensity(i1 & 0xf); + col.green = bitmap_amiga_intensity(i2 >> 4); + col.blue = bitmap_amiga_intensity(i2 & 0xf); + return col; +} + +/* + The Amiga image file has the following format. It consists of a 44 byte + header followed by the image data. + + The header has the following format: + Bytes 0-63: thirty-two entry Amiga palette + Bytes 64-65: padding + Bytes 66-67: big-endian word holding picture width in pixels* + Bytes 68-69: padding + Bytes 70-71: big-endian word holding number of pixel rows in the image* + + [*] these are probably big-endian unsigned longs but I have designated + the upper two bytes as padding because (a) Level 9 does not need + them as longs and (b) using unsigned shorts reduces byte sex induced + byte order juggling. + + The images are designed for an Amiga low-res mode screen - that is they + assume a 320*256 (or 320 * 200 if NSTC display) screen with a palette of + 32 colours from the possible 4096. + + The image data is organised the same way that Amiga video memory is. The + entire data block is divided into five equal length bit planes with the + first bit plane holding the low bit of each 5-bit pixel, the second bitplane + the second bit of the pixel and so on up to the fifth bit plane holding the + high bit of the f5-bit pixel. +*/ +L9BOOL bitmap_amiga_decode(char *file, int x, int y) { + L9BYTE *data = NULL; + int i, xi, yi, max_x, max_y, p, b; + + L9UINT32 size; + data = bitmap_load(file, &size); + if (data == NULL) + return FALSE; + + max_x = (((((data[64] << 8) | data[65]) << 8) | data[66]) << 8) | data[67]; + max_y = (((((data[68] << 8) | data[69]) << 8) | data[70]) << 8) | data[71]; + if (max_x > MAX_BITMAP_WIDTH || max_y > MAX_BITMAP_HEIGHT) { + free(data); + return FALSE; + } + + if ((x == 0) && (y == 0)) { + if (bitmap) + free(bitmap); + bitmap = bitmap_alloc(max_x, max_y); + } + if (bitmap == NULL) { + free(data); + return FALSE; + } + + if (x + max_x > bitmap->width) + max_x = bitmap->width - x; + if (y + max_y > bitmap->height) + max_y = bitmap->height - y; + + for (yi = 0; yi < max_y; yi++) { + for (xi = 0; xi < max_x; xi++) { + p = 0; + for (b = 0; b < 5; b++) + p |= ((data[72 + (max_x / 8) * (max_y * b + yi) + xi / 8] >> (7 - (xi % 8))) & 1) << b; + bitmap->bitmap[(bitmap->width * (y + yi)) + (x + xi)] = p; + } + } + + bitmap->npalette = 32; + for (i = 0; i < 32; i++) + bitmap->palette[i] = bitmap_amiga_colour(data[i * 2], data[i * 2 + 1]); + + free(data); + return TRUE; +} + +BitmapType bitmap_noext_type(char *file) { + Common::File f; + if (f.open(file)) { + L9BYTE data[72]; + int x, y; + + if (f.read(data, sizeof(data)) != sizeof(data) && !f.eos()) + return NO_BITMAPS; + f.close(); + + x = data[67] + data[66] * 256; + y = data[71] + data[70] * 256; + + if ((x == 0x0140) && (y == 0x0088)) + return AMIGA_BITMAPS; + if ((x == 0x0140) && (y == 0x0087)) + return AMIGA_BITMAPS; + if ((x == 0x00E0) && (y == 0x0075)) + return AMIGA_BITMAPS; + if ((x == 0x00E4) && (y == 0x0075)) + return AMIGA_BITMAPS; + if ((x == 0x00E0) && (y == 0x0076)) + return AMIGA_BITMAPS; + if ((x == 0x00DB) && (y == 0x0076)) + return AMIGA_BITMAPS; + + x = data[3] + data[2] * 256; + y = data[7] + data[6] * 256; + + if ((x == 0x0200) && (y == 0x00D8)) + return MAC_BITMAPS; + if ((x == 0x0168) && (y == 0x00BA)) + return MAC_BITMAPS; + if ((x == 0x0168) && (y == 0x00BC)) + return MAC_BITMAPS; + if ((x == 0x0200) && (y == 0x00DA)) + return MAC_BITMAPS; + if ((x == 0x0168) && (y == 0x00DA)) + return MAC_BITMAPS; + + x = data[35] + data[34] * 256; + y = data[39] + data[38] * 256; + + if ((x == 0x0050) && (y == 0x0087)) + return ST1_BITMAPS; + if ((x == 0x0038) && (y == 0x0074)) + return ST1_BITMAPS; + } + + return NO_BITMAPS; +} + +/* + Macintosh Bitmaps +*/ + +/* + The Mac image file format is very simple. The header is ten bytes + with the width of the image in pixels in the first long and the + height (in pixel rows) in the second long - both are big-endian. + (In both cases I treat these as unsigned shorts to minimise byte + twiddling when working around byte sex issues). There follow two + unidentified bytes - possibly image type identifiers or maybe + valid pixel masks for the beginning and end of pixel rows in + sub-images. + + The image data is extremely simple. The entire block is a packed array + of 1-bit pixels - I.E. each byte holds eight pixels - with 1 representing + white and 0 representing black. The pixels are organised with the top + left first and bottom left last, each row in turn. + + The image sizes are 512 * 216 pixels for main images and 360 * 186 pixels + for sub-images. +*/ +L9BOOL bitmap_mac_decode(char *file, int x, int y) { + L9BYTE *data = NULL; + int xi, yi, max_x, max_y; + + L9UINT32 size; + data = bitmap_load(file, &size); + if (data == NULL) + return FALSE; + + max_x = data[3] + data[2] * 256; + max_y = data[7] + data[6] * 256; + if (max_x > MAX_BITMAP_WIDTH || max_y > MAX_BITMAP_HEIGHT) { + free(data); + return FALSE; + } + + if (x > 0) /* Mac bug, apparently */ + x = 78; + + if ((x == 0) && (y == 0)) { + if (bitmap) + free(bitmap); + bitmap = bitmap_alloc(max_x, max_y); + } + if (bitmap == NULL) { + free(data); + return FALSE; + } + + if (x + max_x > bitmap->width) + max_x = bitmap->width - x; + if (y + max_y > bitmap->height) + max_y = bitmap->height - y; + + for (yi = 0; yi < max_y; yi++) { + for (xi = 0; xi < max_x; xi++) { + bitmap->bitmap[(bitmap->width * (y + yi)) + (x + xi)] = + (data[10 + (max_x / 8) * yi + xi / 8] >> (7 - (xi % 8))) & 1; + } + } + + bitmap->npalette = 2; + bitmap->palette[0].red = 0; + bitmap->palette[0].green = 0; + bitmap->palette[0].blue = 0; + bitmap->palette[1].red = 0xff; + bitmap->palette[1].green = 0xff; + bitmap->palette[1].blue = 0xff; + + free(data); + return TRUE; +} + +/* + C64 Bitmaps, also related formats (BBC B, Amstrad CPC and Spectrum +3) +*/ + +/* Commodore 64 palette from Vice */ +const Colour bitmap_c64_colours[] = { + {0x00, 0x00, 0x00 }, + {0xff, 0xff, 0xff }, + {0x89, 0x40, 0x36 }, + {0x7a, 0xbf, 0xc7 }, + {0x8a, 0x46, 0xae }, + {0x68, 0xa9, 0x41 }, + {0x3e, 0x31, 0xa2 }, + {0xd0, 0xdc, 0x71 }, + {0x90, 0x5f, 0x25 }, + {0x5c, 0x47, 0x00 }, + {0xbb, 0x77, 0x6d }, + {0x55, 0x55, 0x55 }, + {0x80, 0x80, 0x80 }, + {0xac, 0xea, 0x88 }, + {0x7c, 0x70, 0xda }, + {0xab, 0xab, 0xab } +}; + +const Colour bitmap_bbc_colours[] = { + {0x00, 0x00, 0x00 }, + {0xff, 0x00, 0x00 }, + {0x00, 0xff, 0x00 }, + {0xff, 0xff, 0x00 }, + {0x00, 0x00, 0xff }, + {0xff, 0x00, 0xff }, + {0x00, 0xff, 0xff }, + {0xff, 0xff, 0xff } +}; + +void bitmap_c64_name(int num, char *dir, char *out) { + if (num == 0) + sprintf(out, "%stitle mpic", dir); + else + sprintf(out, "%spic%d", dir, num); +} + +void bitmap_bbc_name(int num, char *dir, char *out) { + if (num == 0) { + sprintf(out, "%sP.Title", dir); + if (Common::File::exists(out)) + return; + + sprintf(out, "%stitle", dir); + } else { + sprintf(out, "%sP.Pic%d", dir, num); + if (Common::File::exists(out)) + return; + + sprintf(out, "%spic%d", dir, num); + } +} + +void bitmap_cpc_name(int num, char *dir, char *out) { + if (num == 0) + sprintf(out, "%stitle.pic", dir); + else if (num == 1) + sprintf(out, "%s1.pic", dir); + else + sprintf(out, "%sallpics.pic", dir); +} + +BitmapType bitmap_c64_type(char *file) { + BitmapType type = C64_BITMAPS; + + Common::File f; + if (f.open(file)) { + L9UINT32 size = f.size(); + f.close(); + + if (size == 10048) + type = BBC_BITMAPS; + if (size == 6494) + type = BBC_BITMAPS; + } + + return type; +} + +/* + The C64 graphics file format is (loosely) based on the layout of + C64 graphics memory. There are in fact two formats (i) the + standard game images and (ii) title pictures. For both formats + the file begins within the 2-byte pair 0x00 and 0x20. + + The images are "multi-color bitmap mode" images which means they + have rows of 160 double width pixels and can be up to 200 rows + long. (The title images are 200 lines long, the game images are + 136 lines long.) Unlike Amiga, Mac, ST and PC graphics there are + no "main" and "sub" images. All game graphics have the same + dimensions and each completely replaces its predecessor. + + The graphics files used on the Amstrad CPC and Spectrum +3 are also + virtually identical to C64 graphics files. This choice was presumably + made because although the CPC screen was more capable than the c64 it + was (in low resolution) the same size (160*200) and presumably + algorothmic conversion conversion of the colours was trivial for + the interpreter. In addition (a) the artwork already existed so no + extra expense would be incurred and (b) by accepting the C64's + limitation of only four colours in each 4*8 pixel block (but still + with sixteen colours on screen) they got a compressed file format + allowing more pictures on each disk. + + The file organisation is rather different though. Only picture + one and the title picture are separate files. All the other + pictures (2-29) are stored in one large file "allpics.pic". + + On these platforms the picture 1 file and title picture file have + an AMSDOS header (a 128 byte block of metadata) which contains a + checksum of the first 66 bytes of the header in a little-endian + word at bytes 67 & 68. On the original C64 platform there was a + simple two byte header. Following the header the data is organised + exactly as in the C64 game and title image files. The + 'allpics.pic" file has no header and consists of 0x139E blocks + each forming a picture, in the C64 game file format (minus the two + byte header). +*/ +L9BOOL bitmap_c64_decode(char *file, BitmapType type, int num) { + L9BYTE *data = NULL; + int i = 0, xi, yi, max_x = 0, max_y = 0, cx, cy, px, py, p; + int off = 0, off_scr = 0, off_col = 0, off_bg = 0, col_comp = 0; + + L9UINT32 size; + data = bitmap_load(file, &size); + if (data == NULL) + return FALSE; + + if (type == C64_BITMAPS) { + if (size == 10018) { /* C64 title picture */ + max_x = 320; + max_y = 200; + off = 2; + off_scr = 8002; + off_bg = 9003; + off_col = 9018; + col_comp = 0; + } else if (size == 6464) { /* C64 picture */ + max_x = 320; + max_y = 136; + off = 2; + off_scr = 5442; + off_col = 6122; + off_bg = 6463; + col_comp = 1; + } else + return FALSE; + } else if (type == BBC_BITMAPS) { + if (size == 10058) { /* BBC title picture */ + max_x = 320; + max_y = 200; + off = 10; + off_scr = 8010; + off_bg = 9011; + off_col = 9026; + col_comp = 0; + } else if (size == 10048) { /* BBC title picture */ + max_x = 320; + max_y = 200; + off = 0; + off_scr = 8000; + off_bg = 9001; + off_col = 9016; + col_comp = 0; + } else if (size == 6504) { /* BBC picture */ + max_x = 320; + max_y = 136; + off = 10; + off_scr = 5450; + off_col = 6130; + off_bg = 6471; + col_comp = 1; + } else if (size == 6494) { /* BBC picture */ + max_x = 320; + max_y = 136; + off = 0; + off_scr = 5440; + off_col = 6120; + off_bg = 6461; + col_comp = 1; + } else + return FALSE; + } else if (type == CPC_BITMAPS) { + if (num == 0) { /* CPC/+3 title picture */ + max_x = 320; + max_y = 200; + off = 128; + off_scr = 8128; + off_bg = 9128; + off_col = 9144; + col_comp = 0; + } else if (num == 1) { /* First CPC/+3 picture */ + max_x = 320; + max_y = 136; + off = 128; + off_scr = 5568; + off_col = 6248; + off_bg = 6588; + col_comp = 1; + } else if (num >= 2 && num <= 29) { /* Subsequent CPC/+3 pictures */ + max_x = 320; + max_y = 136; + off = ((num - 2) * 6462); + off_scr = 5440 + ((num - 2) * 6462); + off_col = 6120 + ((num - 2) * 6462); + off_bg = 6460 + ((num - 2) * 6462); + col_comp = 1; + } else + return FALSE; + } + + if (bitmap) + free(bitmap); + bitmap = bitmap_alloc(max_x, max_y); + if (bitmap == NULL) { + free(data); + return FALSE; + } + + for (yi = 0; yi < max_y; yi++) { + for (xi = 0; xi < max_x / 2; xi++) { + cx = xi / 4; + px = xi % 4; + cy = yi / 8; + py = yi % 8; + + p = data[off + (cy * 40 + cx) * 8 + py]; + p = (p >> ((3 - px) * 2)) & 3; + + switch (p) { + case 0: + i = data[off_bg] & 0x0f; + break; + case 1: + i = data[off_scr + cy * 40 + cx] >> 4; + break; + case 2: + i = data[off_scr + cy * 40 + cx] & 0x0f; + break; + case 3: + if (col_comp) + i = (data[off_col + (cy * 40 + cx) / 2] >> ((1 - (cx % 2)) * 4)) & 0x0f; + else + i = data[off_col + (cy * 40 + cx)] & 0x0f; + break; + } + + bitmap->bitmap[(bitmap->width * yi) + (xi * 2)] = i; + bitmap->bitmap[(bitmap->width * yi) + (xi * 2) + 1] = i; + } + } + + bitmap->npalette = 16; + for (i = 0; i < 16; i++) + bitmap->palette[i] = bitmap_c64_colours[i]; + + free(data); + return TRUE; +} + +/* + The graphics files used by the BBC B are virtually identical + to C64 graphics files. I assume that (as with the CPC and + Spectrum+3) this choice was made because the BBC mode 2 screen, + was nearly the same size (160*256) and had roughly the same capability + as the C64 screen (displays 16 colours, although eight of those ar + just the first eight flashing). + + In addition (a) the artwork already existed so no extra expense would + be incurred and (b) by accepting the C64's limitation of only four + colours in each 4*8 pixel block (but still with sixteen colours on + screen) they got a compressed file format allowing more pictures + on each disk. + + The file organisation is very close to the C64. The naming system + can be the same eg "PIC12", but another form is also used : + "P.Pic12". Unlike the C64 the BBC has well defined title images, + called "TITLE" or P.Title. All pictures are in separate files. + + The only difference seems to be: + + * There is either *no* header before the image data or a simple + 10 byte header which I think *may* be a file system header + left in place by the extractor system. + + * There is an extra 32 bytes following the data at the end of + each file. These bytes encode a table to convert between the 16 + C64 colours and 16, four-pixel pix-patterns used to let the BBC + (with only 8 colours) represent the sixteen possible C64 colours. + + A pix-pattern looks like this: + + | Even | Odd | + | Column | Column | + ----------------------------- + Even Row |Pixel 1 | Pixel 2 | + ---------|--------|---------| + Odd Row |Pixel 3 | Pixel 4 | + ----------------------------- + + Each of the four pixel *can* be any of the eight BBC Mode 2 + steady colours. In practice they seem either to be all the + same or a simple check of two colours - the pixels in the + odd row being in the reverse order to those in the even row. + + When converting a C64 pixel to a BBC pixel the game uses the + value of the C64 pixel as an index into the array of sixteen + BBC pix-patterns. The game looks at the selected pattern and + chooses the BBC pixel colour thus: if the pixel is in an even + numbered row and an even numbered column, it uses Pixel 1 from + the pattern, if in an even row but an odd column, it uses Pixel 3 + and so on. + + The pix-pattern data is encoded thus: the first sixteen bytes + encode the even row pixels for the patterns, one byte per + pattern, and in the same way the second sixteen bytes encode + the odd row pixels for each pattern. For example for the + pattern representing C64 colour 0 the even row pixels are encoded + in the first byte and the odd row pixels in the sixteenth byte. + + Within each byte the pixels are encoded in this way: + + Bit 7 6 5 4 3 2 1 0 + ------------------------------------- + 0 0 1 0 0 1 1 1 + | | | | | | | | + +---|---+---|---+---|---+---|----- Even Pixel 0101 (5) + | | | | + +-------+-------+-------+----- Odd Pixel 0011 (3) + + This function calls the C64 decoding routines to do the actual + loading. See the comments to that function for details of how the + image is encoded and stored. +*/ +L9BOOL bitmap_bbc_decode(char *file, BitmapType type, int num) { + unsigned char patRowData[32]; + unsigned char patArray[16][2][2]; + int i, j, k, isOddColumn, isOddRow; + L9BYTE pixel; + + if (bitmap_c64_decode(file, type, num) == FALSE) + return FALSE; + + Common::File f; + if (!f.open(file)) + return FALSE; + + /* Seek to the offset of the pixPat data and read in the data */ + f.seek(f.size() - 32, SEEK_SET); + if (f.read(patRowData, 32) != 32 && !f.eos()) + return FALSE; + f.close(); + + /* Extract the patterns */ + i = 0; + for (k = 0; k < 2; k++) { + for (j = 0; j < 16; j++) { + /* Extract the even col pixel for this pattern row */ + patArray[j][k][0] = + ((patRowData[i] >> 4) & 0x8) + ((patRowData[i] >> 3) & 0x4) + + ((patRowData[i] >> 2) & 0x2) + ((patRowData[i] >> 1) & 0x1); + /* Extract the odd col pixel for this pattern row */ + patArray[j][k][1] = + ((patRowData[i] >> 3) & 0x8) + ((patRowData[i] >> 2) & 0x4) + + ((patRowData[i] >> 1) & 0x2) + (patRowData[i] & 0x1); + i++; + } + } + + /* Convert the image. Each BBC pixel is represented by two pixels here */ + i = 0; + isOddRow = 0; + for (j = 0; j < bitmap->height; j++) { + isOddColumn = 0; + for (k = 0; k < bitmap->width / 2; k++) { + pixel = bitmap->bitmap[i]; + bitmap->bitmap[i] = patArray[pixel][isOddColumn][isOddRow]; + bitmap->bitmap[i + 1] = patArray[pixel][isOddColumn][isOddRow]; + isOddColumn ^= 1; + i += 2; + } + isOddRow ^= 1; + } + + bitmap->npalette = 8; + for (i = 0; i < 8; i++) + bitmap->palette[i] = bitmap_bbc_colours[i]; + + return TRUE; +} + +BitmapType DetectBitmaps(char *dir) { + char file[MAX_PATH]; + + bitmap_noext_name(2, dir, file); + if (bitmap_exists(file)) + return bitmap_noext_type(file); + + bitmap_pc_name(2, dir, file); + if (bitmap_exists(file)) + return bitmap_pc_type(file); + + bitmap_c64_name(2, dir, file); + if (bitmap_exists(file)) + return bitmap_c64_type(file); + + bitmap_bbc_name(2, dir, file); + if (bitmap_exists(file)) + return BBC_BITMAPS; + + bitmap_cpc_name(2, dir, file); + if (bitmap_exists(file)) + return CPC_BITMAPS; + + bitmap_st2_name(2, dir, file); + if (bitmap_exists(file)) + return ST2_BITMAPS; + + return NO_BITMAPS; +} + +Bitmap *DecodeBitmap(char *dir, BitmapType type, int num, int x, int y) { + char file[MAX_PATH]; + + switch (type) { + case PC1_BITMAPS: + bitmap_pc_name(num, dir, file); + if (bitmap_pc1_decode(file, x, y)) + return bitmap; + break; + + case PC2_BITMAPS: + bitmap_pc_name(num, dir, file); + if (bitmap_pc2_decode(file, x, y)) + return bitmap; + break; + + case AMIGA_BITMAPS: + bitmap_noext_name(num, dir, file); + if (bitmap_amiga_decode(file, x, y)) + return bitmap; + break; + + case C64_BITMAPS: + bitmap_c64_name(num, dir, file); + if (bitmap_c64_decode(file, type, num)) + return bitmap; + break; + + case BBC_BITMAPS: + bitmap_bbc_name(num, dir, file); + if (bitmap_bbc_decode(file, type, num)) + return bitmap; + break; + + case CPC_BITMAPS: + bitmap_cpc_name(num, dir, file); + if (bitmap_c64_decode(file, type, num)) /* Nearly identical to C64 */ + return bitmap; + break; + + case MAC_BITMAPS: + bitmap_noext_name(num, dir, file); + if (bitmap_mac_decode(file, x, y)) + return bitmap; + break; + + case ST1_BITMAPS: + bitmap_noext_name(num, dir, file); + if (bitmap_st1_decode(file, x, y)) + return bitmap; + break; + + case ST2_BITMAPS: + bitmap_st2_name(num, dir, file); + if (bitmap_pc2_decode(file, x, y)) + return bitmap; + break; + + default: + break; + } + + return NULL; +} + +} // End of namespace Level9 +} // End of namespace Glk diff --git a/engines/glk/level9/level9_main.cpp b/engines/glk/level9/level9_main.cpp new file mode 100644 index 0000000000..719d05bcb8 --- /dev/null +++ b/engines/glk/level9/level9_main.cpp @@ -0,0 +1,3599 @@ +/* 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. + * + */ + +/* + * The input routine will repond to the following 'hash' commands + * #save saves position file directly (bypasses any + * disk change prompts) + * #restore restores position file directly (bypasses any + * protection code) + * #quit terminates current game, RunGame() will return FALSE + * #cheat tries to bypass restore protection on v3,4 games + * (can be slow) + * #dictionary lists game dictionary (press a key to interrupt) + * #picture <n> show picture <n> + * #seed <n> set the random number seed to the value <n> + * #play plays back a file as the input to the game + * +\***********************************************************************/ + +#include "glk/level9/level9_main.h" +#include "glk/level9/level9.h" +#include "common/file.h" +#include "common/system.h" + +namespace Glk { +namespace Level9 { + +/* #define L9DEBUG */ +/* #define CODEFOLLOW */ +/* #define FULLSCAN */ + +/* "L901" */ +#define L9_ID 0x4c393031 + +#define IBUFFSIZE 500 +#define RAMSAVESLOTS 10 +#define GFXSTACKSIZE 100 +#define FIRSTLINESIZE 96 + +/* Typedefs */ +typedef struct { + L9UINT16 vartable[256]; + L9BYTE listarea[LISTAREASIZE]; +} SaveStruct; + +/* Enumerations */ +enum L9GameTypes { L9_V1, L9_V2, L9_V3, L9_V4 }; +enum L9MsgTypes { MSGT_V1, MSGT_V2 }; +/* + Graphics type Resolution Scale stack reset + ------------------------------------------------- + GFX_V2 160 x 128 yes + GFX_V3A 160 x 96 yes + GFX_V3B 160 x 96 no + GFX_V3C 320 x 96 no +*/ +enum L9GfxTypes { GFX_V2, GFX_V3A, GFX_V3B, GFX_V3C }; + +/* Global Variables */ +L9BYTE *startfile = NULL, *pictureaddress = NULL, *picturedata = NULL; +L9BYTE *startdata; +L9UINT32 FileSize, picturesize; + +L9BYTE *L9Pointers[12]; +L9BYTE *absdatablock, *list2ptr, *list3ptr, *list9startptr, *acodeptr; +L9BYTE *startmd, *endmd, *endwdp5, *wordtable, *dictdata, *defdict; +L9UINT16 dictdatalen; +L9BYTE *startmdV2; + +int wordcase; +int unpackcount; +char unpackbuf[8]; +L9BYTE *dictptr; +char threechars[34]; +int L9GameType; +int L9MsgType; +char LastGame[MAX_PATH]; +char FirstLine[FIRSTLINESIZE]; +int FirstLinePos = 0; +int FirstPicture = -1; + +#if defined(AMIGA) && defined(_DCC) +__far SaveStruct ramsavearea[RAMSAVESLOTS]; +#else +SaveStruct ramsavearea[RAMSAVESLOTS]; +#endif + +GameState workspace; + +L9UINT16 randomseed; +L9UINT16 constseed = 0; +L9BOOL Running; + +char ibuff[IBUFFSIZE]; +L9BYTE *ibuffptr; +char obuff[34]; +Common::SeekableReadStream *scriptfile = NULL; + +L9BOOL Cheating = FALSE; +int CheatWord; +GameState CheatWorkspace; + +int reflectflag, scale, gintcolour, option; +int l9textmode = 0, drawx = 0, drawy = 0, screencalled = 0, showtitle = 1; +L9BYTE *gfxa5 = NULL; +Bitmap *bitmap = NULL; +int gfx_mode = GFX_V2; + +L9BYTE *GfxA5Stack[GFXSTACKSIZE]; +int GfxA5StackPos = 0; +int GfxScaleStack[GFXSTACKSIZE]; +int GfxScaleStackPos = 0; + +char lastchar = '.'; +char lastactualchar = 0; +int d5; + +L9BYTE *codeptr; /* instruction codes */ +L9BYTE code; + +L9BYTE *list9ptr; + +int unpackd3; + +L9BYTE exitreversaltable[16] = {0x00, 0x04, 0x06, 0x07, 0x01, 0x08, 0x02, 0x03, 0x05, 0x0a, 0x09, 0x0c, 0x0b, 0xff, 0xff, 0x0f}; + +L9UINT16 gnostack[128]; +L9BYTE gnoscratch[32]; +int object, gnosp, numobjectfound, searchdepth, inithisearchpos; + + +struct L9V1GameInfo { + L9BYTE dictVal1, dictVal2; + int dictStart, L9Ptrs[5], absData, msgStart, msgLen; +}; +struct L9V1GameInfo L9V1Games[] = { + 0x1a, 0x24, 301, 0x0000, -0x004b, 0x0080, -0x002b, 0x00d0, 0x03b0, 0x0f80, 0x4857, /* Colossal Adventure */ + 0x20, 0x3b, 283, -0x0583, 0x0000, -0x0508, -0x04e0, 0x0000, 0x0800, 0x1000, 0x39d1, /* Adventure Quest */ + 0x14, 0xff, 153, -0x00d6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0a20, 0x16bf, 0x420d, /* Dungeon Adventure */ + 0x15, 0x5d, 252, -0x3e70, 0x0000, -0x3d30, -0x3ca0, 0x0100, 0x4120, -0x3b9d, 0x3988, /* Lords of Time */ + 0x15, 0x6c, 284, -0x00f0, 0x0000, -0x0050, -0x0050, -0x0050, 0x0300, 0x1930, 0x3c17, /* Snowball */ +}; +int L9V1Game = -1; + + +/* Prototypes */ +L9BOOL LoadGame2(char *filename, char *picname); +int getlongcode(void); +L9BOOL GetWordV2(char *buff, int Word); +L9BOOL GetWordV3(char *buff, int Word); +void show_picture(int pic); + + +#ifdef CODEFOLLOW +#define CODEFOLLOWFILE "c:\\temp\\level9.txt" +FILE *f; +L9UINT16 *cfvar, *cfvar2; +char *codes[] = { + "Goto", + "intgosub", + "intreturn", + "printnumber", + "messagev", + "messagec", + "function", + "input", + "varcon", + "varvar", + "_add", + "_sub", + "ilins", + "ilins", + "jump", + "Exit", + "ifeqvt", + "ifnevt", + "ifltvt", + "ifgtvt", + "screen", + "cleartg", + "picture", + "getnextobject", + "ifeqct", + "ifnect", + "ifltct", + "ifgtct", + "printinput", + "ilins", + "ilins", + "ilins", +}; +char *functions[] = { + "calldriver", + "L9Random", + "save", + "restore", + "clearworkspace", + "clearstack" +}; +char *drivercalls[] = { + "init", + "drivercalcchecksum", + "driveroswrch", + "driverosrdch", + "driverinputline", + "driversavefile", + "driverloadfile", + "settext", + "resettask", + "returntogem", + "10 *", + "loadgamedatafile", + "randomnumber", + "13 *", + "driver14", + "15 *", + "driverclg", + "line", + "fill", + "driverchgcol", + "20 *", + "21 *", + "ramsave", + "ramload", + "24 *", + "lensdisplay", + "26 *", + "27 *", + "28 *", + "29 *", + "allocspace", + "31 *", + "showbitmap", + "33 *", + "checkfordisc" +}; +#endif + +void initdict(L9BYTE *ptr) { + dictptr = ptr; + unpackcount = 8; +} + +char getdictionarycode(void) { + if (unpackcount != 8) return unpackbuf[unpackcount++]; + else { + /* unpackbytes */ + L9BYTE d1 = *dictptr++, d2; + unpackbuf[0] = d1 >> 3; + d2 = *dictptr++; + unpackbuf[1] = ((d2 >> 6) + (d1 << 2)) & 0x1f; + d1 = *dictptr++; + unpackbuf[2] = (d2 >> 1) & 0x1f; + unpackbuf[3] = ((d1 >> 4) + (d2 << 4)) & 0x1f; + d2 = *dictptr++; + unpackbuf[4] = ((d1 << 1) + (d2 >> 7)) & 0x1f; + d1 = *dictptr++; + unpackbuf[5] = (d2 >> 2) & 0x1f; + unpackbuf[6] = ((d2 << 3) + (d1 >> 5)) & 0x1f; + unpackbuf[7] = d1 & 0x1f; + unpackcount = 1; + return unpackbuf[0]; + } +} + +int getdictionary(int d0) { + if (d0 >= 0x1a) return getlongcode(); + else return d0 + 0x61; +} + +int getlongcode(void) { + int d0, d1; + d0 = getdictionarycode(); + if (d0 == 0x10) { + wordcase = 1; + d0 = getdictionarycode(); + return getdictionary(d0); /* reentrant? */ + } + d1 = getdictionarycode(); + return 0x80 | ((d0 << 5) & 0xe0) | (d1 & 0x1f); +} + +void printchar(char c) { + if (Cheating) return; + + if (c & 128) + lastchar = (c &= 0x7f); + else if (c != 0x20 && c != 0x0d && (c < '\"' || c >= '.')) { + if (lastchar == '!' || lastchar == '?' || lastchar == '.') c = toupper(c); + lastchar = c; + } + /* eat multiple CRs */ + if (c != 0x0d || lastactualchar != 0x0d) { + os_printchar(c); + if (FirstLinePos < FIRSTLINESIZE - 1) + FirstLine[FirstLinePos++] = tolower(c); + } + lastactualchar = c; +} + +void printstring(const char *buf) { + int i; + for (i = 0; i < (int) strlen(buf); i++) printchar(buf[i]); +} + +void printdecimald0(int d0) { + char temp[12]; + sprintf(temp, "%d", d0); + printstring(temp); +} + +void printautocase(int d0) { + if (d0 & 128) printchar((char) d0); + else { + if (wordcase) printchar((char) toupper(d0)); + else if (d5 < 6) printchar((char) d0); + else { + wordcase = 0; + printchar((char) toupper(d0)); + } + } +} + +void displaywordref(L9UINT16 Off) { + static int mdtmode = 0; + + wordcase = 0; + d5 = (Off >> 12) & 7; + Off &= 0xfff; + if (Off < 0xf80) { + /* dwr01 */ + L9BYTE *a0, *oPtr, *a3; + int d0, d2, i; + + if (mdtmode == 1) printchar(0x20); + mdtmode = 1; + + /* setindex */ + a0 = dictdata; + d2 = dictdatalen; + + /* dwr02 */ + oPtr = a0; + while (d2 && Off >= L9WORD(a0 + 2)) { + a0 += 4; + d2--; + } + /* dwr04 */ + if (a0 == oPtr) { + a0 = defdict; + } else { + a0 -= 4; + Off -= L9WORD(a0 + 2); + a0 = startdata + L9WORD(a0); + } + /* dwr04b */ + Off++; + initdict(a0); + a3 = (L9BYTE *) threechars; /* a3 not set in original, prevent possible spam */ + + /* dwr05 */ + while (TRUE) { + d0 = getdictionarycode(); + if (d0 < 0x1c) { + /* dwr06 */ + if (d0 >= 0x1a) d0 = getlongcode(); + else d0 += 0x61; + *a3++ = d0; + } else { + d0 &= 3; + a3 = (L9BYTE *) threechars + d0; + if (--Off == 0) break; + } + } + for (i = 0; i < d0; i++) printautocase(threechars[i]); + + /* dwr10 */ + while (TRUE) { + d0 = getdictionarycode(); + if (d0 >= 0x1b) return; + printautocase(getdictionary(d0)); + } + } + + else { + if (d5 & 2) printchar(0x20); /* prespace */ + mdtmode = 2; + Off &= 0x7f; + if (Off != 0x7e) printchar((char)Off); + if (d5 & 1) printchar(0x20); /* postspace */ + } +} + +int getmdlength(L9BYTE **Ptr) { + int tot = 0, len; + do { + len = (*(*Ptr)++ -1) & 0x3f; + tot += len; + } while (len == 0x3f); + return tot; +} + +void printmessage(int Msg) { + L9BYTE *Msgptr = startmd; + L9BYTE Data; + + int len; + L9UINT16 Off; + + while (Msg > 0 && Msgptr - endmd <= 0) { + Data = *Msgptr; + if (Data & 128) { + Msgptr++; + Msg -= Data & 0x7f; + } else { + len = getmdlength(&Msgptr); + Msgptr += len; + } + Msg--; + } + if (Msg < 0 || *Msgptr & 128) return; + + len = getmdlength(&Msgptr); + if (len == 0) return; + + while (len) { + Data = *Msgptr++; + len--; + if (Data & 128) { + /* long form (reverse word) */ + Off = (Data << 8) + *Msgptr++; + len--; + } else { + Off = (wordtable[Data * 2] << 8) + wordtable[Data * 2 + 1]; + } + if (Off == 0x8f80) break; + displaywordref(Off); + } +} + +/* v2 message stuff */ + +int msglenV2(L9BYTE **ptr) { + int i = 0; + L9BYTE a; + + /* catch berzerking code */ + if (*ptr >= startdata + FileSize) return 0; + + while ((a = **ptr) == 0) { + (*ptr)++; + + if (*ptr >= startdata + FileSize) return 0; + + i += 255; + } + i += a; + return i; +} + +void printcharV2(char c) { + if (c == 0x25) c = 0xd; + else if (c == 0x5f) c = 0x20; + printautocase(c); +} + +void displaywordV2(L9BYTE *ptr, int msg) { + int n; + L9BYTE a; + if (msg == 0) return; + while (--msg) { + ptr += msglenV2(&ptr); + } + n = msglenV2(&ptr); + + while (--n > 0) { + a = *++ptr; + if (a < 3) return; + + if (a >= 0x5e) displaywordV2(startmdV2 - 1, a - 0x5d); + else printcharV2((char)(a + 0x1d)); + } +} + +int msglenV1(L9BYTE **ptr) { + L9BYTE *ptr2 = *ptr; + while (ptr2 < startdata + FileSize && *ptr2++ != 1); + return ptr2 - *ptr; +} + +void displaywordV1(L9BYTE *ptr, int msg) { + int n; + L9BYTE a; + while (msg--) { + ptr += msglenV1(&ptr); + } + n = msglenV1(&ptr); + + while (--n > 0) { + a = *ptr++; + if (a < 3) return; + + if (a >= 0x5e) displaywordV1(startmdV2, a - 0x5e); + else printcharV2((char)(a + 0x1d)); + } +} + +L9BOOL amessageV2(L9BYTE *ptr, int msg, long *w, long *c) { + int n; + L9BYTE a; + static int depth = 0; + if (msg == 0) return FALSE; + while (--msg) { + ptr += msglenV2(&ptr); + } + if (ptr >= startdata + FileSize) return FALSE; + n = msglenV2(&ptr); + + while (--n > 0) { + a = *++ptr; + if (a < 3) return TRUE; + + if (a >= 0x5e) { + if (++depth > 10 || !amessageV2(startmdV2 - 1, a - 0x5d, w, c)) { + depth--; + return FALSE; + } + depth--; + } else { + char ch = a + 0x1d; + if (ch == 0x5f || ch == ' ')(*w)++; + else (*c)++; + } + } + return TRUE; +} + +L9BOOL amessageV1(L9BYTE *ptr, int msg, long *w, long *c) { + int n; + L9BYTE a; + static int depth = 0; + + while (msg--) { + ptr += msglenV1(&ptr); + } + if (ptr >= startdata + FileSize) return FALSE; + n = msglenV1(&ptr); + + while (--n > 0) { + a = *ptr++; + if (a < 3) return TRUE; + + if (a >= 0x5e) { + if (++depth > 10 || !amessageV1(startmdV2, a - 0x5e, w, c)) { + depth--; + return FALSE; + } + depth--; + } else { + char ch = a + 0x1d; + if (ch == 0x5f || ch == ' ')(*w)++; + else (*c)++; + } + } + return TRUE; +} + +L9BOOL analyseV2(double *wl) { + long words = 0, chars = 0; + int i; + for (i = 1; i < 256; i++) { + long w = 0, c = 0; + if (amessageV2(startmd, i, &w, &c)) { + words += w; + chars += c; + } else return FALSE; + } + *wl = words ? (double) chars / words : 0.0; + return TRUE; +} + +L9BOOL analyseV1(double *wl) { + long words = 0, chars = 0; + int i; + for (i = 0; i < 256; i++) { + long w = 0, c = 0; + if (amessageV1(startmd, i, &w, &c)) { + words += w; + chars += c; + } else return FALSE; + } + + *wl = words ? (double) chars / words : 0.0; + return TRUE; +} + +void printmessageV2(int Msg) { + if (L9MsgType == MSGT_V2) displaywordV2(startmd, Msg); + else displaywordV1(startmd, Msg); +} + +void L9Allocate(L9BYTE **ptr, L9UINT32 Size) { + if (*ptr) free(*ptr); + *ptr = (L9BYTE *)malloc(Size); + if (*ptr == NULL) { + error("Unable to allocate memory for the game! Exiting..."); + } +} + +void FreeMemory(void) { + if (startfile) { + free(startfile); + startfile = NULL; + } + if (pictureaddress) { + free(pictureaddress); + pictureaddress = NULL; + } + if (bitmap) { + free(bitmap); + bitmap = NULL; + } + if (scriptfile) { + delete scriptfile; + scriptfile = NULL; + } + picturedata = NULL; + picturesize = 0; + gfxa5 = NULL; +} + +L9BOOL load(const char *filename) { + Common::File f; + if (!f.open(filename)) + return FALSE; + + if ((FileSize = f.size()) < 256) { + f.close(); + error("File is too small to contain a Level 9 game"); + return FALSE; + } + + L9Allocate(&startfile, FileSize); + if (f.read(startfile, FileSize) != FileSize) { + f.close(); + return FALSE; + } + + f.close(); + return TRUE; +} + +L9UINT16 scanmovewa5d0(L9BYTE *Base, L9UINT32 *Pos) { + L9UINT16 ret = L9WORD(Base + *Pos); + (*Pos) += 2; + return ret; +} + +L9UINT32 scangetaddr(int Code, L9BYTE *Base, L9UINT32 *Pos, L9UINT32 acode, int *Mask) { + (*Mask) |= 0x20; + if (Code & 0x20) { + /* getaddrshort */ + signed char diff = Base[*Pos]; + (*Pos)++; + return (*Pos) + diff - 1; + } else { + return acode + scanmovewa5d0(Base, Pos); + } +} + +void scangetcon(int Code, L9UINT32 *Pos, int *Mask) { + (*Pos)++; + if (!(Code & 64))(*Pos)++; + (*Mask) |= 0x40; +} + +L9BOOL CheckCallDriverV4(L9BYTE *Base, L9UINT32 Pos) { + int i, j; + + /* Look back for an assignment from a variable + * to list9[0], which is used to specify the + * driver call. + */ + for (i = 0; i < 2; i++) { + int x = Pos - ((i + 1) * 3); + if ((Base[x] == 0x89) && (Base[x + 1] == 0x00)) { + /* Get the variable being copied to list9[0] */ + int var = Base[x + 2]; + + /* Look back for an assignment to the variable. */ + for (j = 0; j < 2; j++) { + int y = x - ((j + 1) * 3); + if ((Base[y] == 0x48) && (Base[y + 2] == var)) { + /* If this a V4 driver call? */ + switch (Base[y + 1]) { + case 0x0E: + case 0x20: + case 0x22: + return TRUE; + } + return FALSE; + } + } + } + } + return FALSE; +} + +L9BOOL ValidateSequence(L9BYTE *Base, L9BYTE *Image, L9UINT32 iPos, L9UINT32 acode, L9UINT32 *Size, L9UINT32 size, L9UINT32 *Min, L9UINT32 *Max, L9BOOL Rts, L9BOOL *JumpKill, L9BOOL *DriverV4) { + L9UINT32 Pos; + L9BOOL Finished = FALSE, Valid; + L9UINT32 Strange = 0; + int ScanCodeMask; + int Code; + *JumpKill = FALSE; + + if (iPos >= size) + return FALSE; + Pos = iPos; + if (Pos < *Min) *Min = Pos; + + if (Image[Pos]) return TRUE; /* hit valid code */ + + do { + Code = Base[Pos]; + Valid = TRUE; + if (Image[Pos]) break; /* converged to found code */ + Image[Pos++] = 2; + if (Pos > *Max) *Max = Pos; + + ScanCodeMask = 0x9f; + if (Code & 0x80) { + ScanCodeMask = 0xff; + if ((Code & 0x1f) > 0xa) + Valid = FALSE; + Pos += 2; + } else switch (Code & 0x1f) { + case 0: { /* goto */ + L9UINT32 Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask); + Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, TRUE/*Rts*/, JumpKill, DriverV4); + Finished = TRUE; + break; + } + case 1: { /* intgosub */ + L9UINT32 Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask); + Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, TRUE, JumpKill, DriverV4); + break; + } + case 2: /* intreturn */ + Valid = Rts; + Finished = TRUE; + break; + case 3: /* printnumber */ + Pos++; + break; + case 4: /* messagev */ + Pos++; + break; + case 5: /* messagec */ + scangetcon(Code, &Pos, &ScanCodeMask); + break; + case 6: /* function */ + switch (Base[Pos++]) { + case 2:/* random */ + Pos++; + break; + case 1:/* calldriver */ + if (DriverV4) { + if (CheckCallDriverV4(Base, Pos - 2)) + *DriverV4 = TRUE; + } + break; + case 3:/* save */ + case 4:/* restore */ + case 5:/* clearworkspace */ + case 6:/* clear stack */ + break; + case 250: /* printstr */ + while (Base[Pos++]); + break; + + default: +#ifdef L9DEBUG + /* printf("scan: illegal function call: %d",Base[Pos-1]); */ +#endif + Valid = FALSE; + break; + } + break; + case 7: /* input */ + Pos += 4; + break; + case 8: /* varcon */ + scangetcon(Code, &Pos, &ScanCodeMask); + Pos++; + break; + case 9: /* varvar */ + Pos += 2; + break; + case 10: /* _add */ + Pos += 2; + break; + case 11: /* _sub */ + Pos += 2; + break; + case 14: /* jump */ +#ifdef L9DEBUG + /* printf("jmp at codestart: %ld",acode); */ +#endif + *JumpKill = TRUE; + Finished = TRUE; + break; + case 15: /* exit */ + Pos += 4; + break; + case 16: /* ifeqvt */ + case 17: /* ifnevt */ + case 18: /* ifltvt */ + case 19: { /* ifgtvt */ + L9UINT32 Val; + Pos += 2; + Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask); + Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, Rts, JumpKill, DriverV4); + break; + } + case 20: /* screen */ + if (Base[Pos++]) Pos++; + break; + case 21: /* cleartg */ + Pos++; + break; + case 22: /* picture */ + Pos++; + break; + case 23: /* getnextobject */ + Pos += 6; + break; + case 24: /* ifeqct */ + case 25: /* ifnect */ + case 26: /* ifltct */ + case 27: { /* ifgtct */ + L9UINT32 Val; + Pos++; + scangetcon(Code, &Pos, &ScanCodeMask); + Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask); + Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, Rts, JumpKill, DriverV4); + break; + } + case 28: /* printinput */ + break; + case 12: /* ilins */ + case 13: /* ilins */ + case 29: /* ilins */ + case 30: /* ilins */ + case 31: /* ilins */ +#ifdef L9DEBUG + /* printf("scan: illegal instruction"); */ +#endif + Valid = FALSE; + break; + } + if (Valid && (Code & ~ScanCodeMask)) + Strange++; + } while (Valid && !Finished && Pos < size); /* && Strange==0); */ + (*Size) += Pos - iPos; + return Valid; /* && Strange==0; */ +} + +L9BYTE calcchecksum(L9BYTE *ptr, L9UINT32 num) { + L9BYTE d1 = 0; + while (num-- != 0) d1 += *ptr++; + return d1; +} + +/* +L9BOOL Check(L9BYTE* StartFile,L9UINT32 FileSize,L9UINT32 Offset) +{ + L9UINT16 d0,num; + int i; + L9BYTE* Image; + L9UINT32 Size=0,Min,Max; + L9BOOL ret,JumpKill; + + for (i=0;i<12;i++) + { + d0=L9WORD (StartFile+Offset+0x12 + i*2); + if (d0>=0x8000+LISTAREASIZE) return FALSE; + } + + num=L9WORD(StartFile+Offset)+1; + if (Offset+num>FileSize) return FALSE; + if (calcchecksum(StartFile+Offset,num)) return FALSE; + + Image=calloc(FileSize,1); + + Min=Max=Offset+d0; + ret=ValidateSequence(StartFile,Image,Offset+d0,Offset+d0,&Size,FileSize,&Min,&Max,FALSE,&JumpKill,NULL); + free(Image); + return ret; +} +*/ + +long Scan(L9BYTE *StartFile, L9UINT32 size) { + L9BYTE *Chk = (L9BYTE *)malloc(size + 1); + L9BYTE *Image = (L9BYTE *)calloc(size, 1); + L9UINT32 i, num, Size, MaxSize = 0; + int j; + L9UINT16 d0 = 0, l9, md, ml, dd, dl; + L9UINT32 Min, Max; + long Offset = -1; + L9BOOL JumpKill, DriverV4; + + if ((Chk == NULL) || (Image == NULL)) { + error("Unable to allocate memory for game scan! Exiting..."); + } + + Chk[0] = 0; + for (i = 1; i <= size; i++) + Chk[i] = Chk[i - 1] + StartFile[i - 1]; + + for (i = 0; i < size - 33; i++) { + num = L9WORD(StartFile + i) + 1; + /* + Chk[i] = 0 +...+ i-1 + Chk[i+n] = 0 +...+ i+n-1 + Chk[i+n] - Chk[i] = i + ... + i+n + */ + if (num > 0x2000 && i + num <= size && Chk[i + num] == Chk[i]) { + md = L9WORD(StartFile + i + 0x2); + ml = L9WORD(StartFile + i + 0x4); + dd = L9WORD(StartFile + i + 0xa); + dl = L9WORD(StartFile + i + 0xc); + + if (ml > 0 && md > 0 && i + md + ml <= size && dd > 0 && dl > 0 && i + dd + dl * 4 <= size) { + /* v4 files may have acodeptr in 8000-9000, need to fix */ + for (j = 0; j < 12; j++) { + d0 = L9WORD(StartFile + i + 0x12 + j * 2); + if (j != 11 && d0 >= 0x8000 && d0 < 0x9000) { + if (d0 >= 0x8000 + LISTAREASIZE) break; + } else if (i + d0 > size) break; + } + /* list9 ptr must be in listarea, acode ptr in data */ + if (j < 12 /*|| (d0>=0x8000 && d0<0x9000)*/) continue; + + l9 = L9WORD(StartFile + i + 0x12 + 10 * 2); + if (l9 < 0x8000 || l9 >= 0x8000 + LISTAREASIZE) continue; + + Size = 0; + Min = Max = i + d0; + DriverV4 = 0; + if (ValidateSequence(StartFile, Image, i + d0, i + d0, &Size, size, &Min, &Max, FALSE, &JumpKill, &DriverV4)) { +#ifdef L9DEBUG + printf("Found valid header at %ld, code size %ld", i, Size); +#endif + if (Size > MaxSize && Size > 100) { + Offset = i; + MaxSize = Size; + L9GameType = DriverV4 ? L9_V4 : L9_V3; + } + } + } + } + } + free(Chk); + free(Image); + return Offset; +} + +long ScanV2(L9BYTE *StartFile, L9UINT32 size) { + L9BYTE *Chk = (L9BYTE *)malloc(size + 1); + L9BYTE *Image = (L9BYTE *)calloc(size, 1); + L9UINT32 i, Size, MaxSize = 0, num; + int j; + L9UINT16 d0 = 0, l9; + L9UINT32 Min, Max; + long Offset = -1; + L9BOOL JumpKill; + + if ((Chk == NULL) || (Image == NULL)) { + error("Unable to allocate memory for game scan! Exiting..."); + } + + Chk[0] = 0; + for (i = 1; i <= size; i++) + Chk[i] = Chk[i - 1] + StartFile[i - 1]; + + for (i = 0; i < size - 28; i++) { + num = L9WORD(StartFile + i + 28) + 1; + if (i + num <= size && ((Chk[i + num] - Chk[i + 32]) & 0xff) == StartFile[i + 0x1e]) { + for (j = 0; j < 14; j++) { + d0 = L9WORD(StartFile + i + j * 2); + if (j != 13 && d0 >= 0x8000 && d0 < 0x9000) { + if (d0 >= 0x8000 + LISTAREASIZE) break; + } else if (i + d0 > size) break; + } + /* list9 ptr must be in listarea, acode ptr in data */ + if (j < 14 /*|| (d0>=0x8000 && d0<0x9000)*/) continue; + + l9 = L9WORD(StartFile + i + 6 + 9 * 2); + if (l9 < 0x8000 || l9 >= 0x8000 + LISTAREASIZE) continue; + + Size = 0; + Min = Max = i + d0; + if (ValidateSequence(StartFile, Image, i + d0, i + d0, &Size, size, &Min, &Max, FALSE, &JumpKill, NULL)) { +#ifdef L9DEBUG + printf("Found valid V2 header at %ld, code size %ld", i, Size); +#endif + if (Size > MaxSize && Size > 100) { + Offset = i; + MaxSize = Size; + } + } + } + } + free(Chk); + free(Image); + return Offset; +} + +long ScanV1(L9BYTE *StartFile, L9UINT32 size) { + L9BYTE *Image = (L9BYTE *)calloc(size, 1); + L9UINT32 i, Size; + int Replace; + L9BYTE *ImagePtr; + long MaxPos = -1; + L9UINT32 MaxCount = 0; + L9UINT32 Min, Max; //, MaxMax, MaxMin; + L9BOOL JumpKill; // , MaxJK; + + int dictOff1 = 0, dictOff2 = 0; + L9BYTE dictVal1 = 0xff, dictVal2 = 0xff; + + if (Image == NULL) { + error("Unable to allocate memory for game scan! Exiting..."); + } + + for (i = 0; i < size; i++) { + if ((StartFile[i] == 0 && StartFile[i + 1] == 6) || (StartFile[i] == 32 && StartFile[i + 1] == 4)) { + Size = 0; + Min = Max = i; + Replace = 0; + if (ValidateSequence(StartFile, Image, i, i, &Size, size, &Min, &Max, FALSE, &JumpKill, NULL)) { + if (Size > MaxCount && Size > 100 && Size < 10000) { + MaxCount = Size; + //MaxMin = Min; + //MaxMax = Max; + + MaxPos = i; + //MaxJK = JumpKill; + } + Replace = 0; + } + for (ImagePtr = Image + Min; ImagePtr <= Image + Max; ImagePtr++) { + if (*ImagePtr == 2) + *ImagePtr = Replace; + } + } + } +#ifdef L9DEBUG + printf("V1scan found code at %ld size %ld", MaxPos, MaxCount); +#endif + + /* V1 dictionary detection from L9Cut by Paul David Doherty */ + for (i = 0; i < size - 20; i++) { + if (StartFile[i] == 'A') { + if (StartFile[i + 1] == 'T' && StartFile[i + 2] == 'T' && StartFile[i + 3] == 'A' && StartFile[i + 4] == 'C' && StartFile[i + 5] == 0xcb) { + dictOff1 = i; + dictVal1 = StartFile[dictOff1 + 6]; + break; + } + } + } + for (i = dictOff1; i < size - 20; i++) { + if (StartFile[i] == 'B') { + if (StartFile[i + 1] == 'U' && StartFile[i + 2] == 'N' && StartFile[i + 3] == 'C' && StartFile[i + 4] == 0xc8) { + dictOff2 = i; + dictVal2 = StartFile[dictOff2 + 5]; + break; + } + } + } + L9V1Game = -1; + if (dictVal1 != 0xff || dictVal2 != 0xff) { + for (i = 0; i < sizeof L9V1Games / sizeof L9V1Games[0]; i++) { + if ((L9V1Games[i].dictVal1 == dictVal1) && (L9V1Games[i].dictVal2 == dictVal2)) { + L9V1Game = i; + dictdata = StartFile + dictOff1 - L9V1Games[i].dictStart; + } + } + } + +#ifdef L9DEBUG + if (L9V1Game >= 0) + printf("V1scan found known dictionary: %d", L9V1Game); +#endif + + free(Image); + + if (MaxPos > 0) { + acodeptr = StartFile + MaxPos; + return 0; + } + return -1; +} + +#ifdef FULLSCAN +void FullScan(L9BYTE *StartFile, L9UINT32 size) { + L9BYTE *Image = (L9BYTE *)calloc(size, 1); + L9UINT32 i, Size; + int Replace; + L9BYTE *ImagePtr; + L9UINT32 MaxPos = 0; + L9UINT32 MaxCount = 0; + L9UINT32 Min, Max, MaxMin, MaxMax; + int Offset; + L9BOOL JumpKill, MaxJK; + for (i = 0; i < size; i++) { + Size = 0; + Min = Max = i; + Replace = 0; + if (ValidateSequence(StartFile, Image, i, i, &Size, size, &Min, &Max, FALSE, &JumpKill, NULL)) { + if (Size > MaxCount) { + MaxCount = Size; + MaxMin = Min; + MaxMax = Max; + + MaxPos = i; + MaxJK = JumpKill; + } + Replace = 0; + } + for (ImagePtr = Image + Min; ImagePtr <= Image + Max; ImagePtr++) { + if (*ImagePtr == 2) + *ImagePtr = Replace; + } + } + printf("%ld %ld %ld %ld %s", MaxPos, MaxCount, MaxMin, MaxMax, MaxJK ? "jmp killed" : ""); + /* search for reference to MaxPos */ + Offset = 0x12 + 11 * 2; + for (i = 0; i < size - Offset - 1; i++) { + if ((L9WORD(StartFile + i + Offset)) + i == MaxPos) { + printf("possible v3,4 Code reference at : %ld", i); + /* startdata=StartFile+i; */ + } + } + Offset = 13 * 2; + for (i = 0; i < size - Offset - 1; i++) { + if ((L9WORD(StartFile + i + Offset)) + i == MaxPos) + printf("possible v2 Code reference at : %ld", i); + } + free(Image); +} +#endif + +L9BOOL findsubs(L9BYTE *testptr, L9UINT32 testsize, L9BYTE **picdata, L9UINT32 *picsize) { + int i, j, length, count; + L9BYTE *picptr, *startptr, *tmpptr; + + if (testsize < 16) return FALSE; + + /* + Try to traverse the graphics subroutines. + + Each subroutine starts with a header: nn | nl | ll + nnn : the subroutine number ( 0x000 - 0x7ff ) + lll : the subroutine length ( 0x004 - 0x3ff ) + + The first subroutine usually has the number 0x000. + Each subroutine ends with 0xff. + + findsubs() searches for the header of the second subroutine + (pattern: 0xff | nn | nl | ll) and then tries to find the + first and next subroutines by evaluating the length fields + of the subroutine headers. + */ + for (i = 4; i < (int)(testsize - 4); i++) { + picptr = testptr + i; + if (*(picptr - 1) != 0xff || (*picptr & 0x80) || (*(picptr + 1) & 0x0c) || (*(picptr + 2) < 4)) + continue; + + count = 0; + startptr = picptr; + + while (TRUE) { + length = ((*(picptr + 1) & 0x0f) << 8) + *(picptr + 2); + if (length > 0x3ff || picptr + length + 4 > testptr + testsize) + break; + + picptr += length; + if (*(picptr - 1) != 0xff) { + picptr -= length; + break; + } + if ((*picptr & 0x80) || (*(picptr + 1) & 0x0c) || (*(picptr + 2) < 4)) + break; + + count++; + } + + if (count > 10) { + /* Search for the start of the first subroutine */ + for (j = 4; j < 0x3ff; j++) { + tmpptr = startptr - j; + if (*tmpptr == 0xff || tmpptr < testptr) + break; + + length = ((*(tmpptr + 1) & 0x0f) << 8) + *(tmpptr + 2); + if (tmpptr + length == startptr) { + startptr = tmpptr; + break; + } + } + + if (*tmpptr != 0xff) { + *picdata = startptr; + *picsize = picptr - startptr; + return TRUE; + } + } + } + return FALSE; +} + +L9BOOL intinitialise(const char *filename, char *picname) { + /* init */ + /* driverclg */ + + int i; + int hdoffset; + long Offset; + Common::File f; + + if (pictureaddress) { + free(pictureaddress); + pictureaddress = NULL; + } + picturedata = NULL; + picturesize = 0; + gfxa5 = NULL; + + if (!load(filename)) { + error("\rUnable to load: %s\r", filename); + return FALSE; + } + + /* try to load graphics */ + if (picname) { + if (f.open(picname)) { + picturesize = f.size(); + L9Allocate(&pictureaddress, picturesize); + if (f.read(pictureaddress, picturesize) != picturesize) { + free(pictureaddress); + pictureaddress = NULL; + picturesize = 0; + } + f.close(); + } + } + screencalled = 0; + l9textmode = 0; + +#ifdef FULLSCAN + FullScan(startfile, FileSize); +#endif + + Offset = Scan(startfile, FileSize); + if (Offset < 0) { + Offset = ScanV2(startfile, FileSize); + L9GameType = L9_V2; + if (Offset < 0) { + Offset = ScanV1(startfile, FileSize); + L9GameType = L9_V1; + if (Offset < 0) { + error("\rUnable to locate valid Level 9 game in file: %s\r", filename); + return FALSE; + } + } + } + + startdata = startfile + Offset; + FileSize -= Offset; + + /* setup pointers */ + if (L9GameType == L9_V1) { + if (L9V1Game < 0) { + error("\rWhat appears to be V1 game data was found, but the game was not recognised.\rEither this is an unknown V1 game file or, more likely, it is corrupted.\r"); + return FALSE; + } + for (i = 0; i < 6; i++) { + int off = L9V1Games[L9V1Game].L9Ptrs[i]; + if (off < 0) + L9Pointers[i + 2] = acodeptr + off; + else + L9Pointers[i + 2] = workspace.listarea + off; + } + absdatablock = acodeptr - L9V1Games[L9V1Game].absData; + } else { + /* V2,V3,V4 */ + hdoffset = L9GameType == L9_V2 ? 4 : 0x12; + for (i = 0; i < 12; i++) { + L9UINT16 d0 = L9WORD(startdata + hdoffset + i * 2); + L9Pointers[i] = (i != 11 && d0 >= 0x8000 && d0 <= 0x9000) ? workspace.listarea + d0 - 0x8000 : startdata + d0; + } + absdatablock = L9Pointers[0]; + dictdata = L9Pointers[1]; + list2ptr = L9Pointers[3]; + list3ptr = L9Pointers[4]; + /*list9startptr */ + list9startptr = L9Pointers[10]; + acodeptr = L9Pointers[11]; + } + + switch (L9GameType) { + case L9_V1: { + double a1; + startmd = acodeptr + L9V1Games[L9V1Game].msgStart; + startmdV2 = startmd + L9V1Games[L9V1Game].msgLen; + + if (analyseV1(&a1) && a1 > 2 && a1 < 10) { + L9MsgType = MSGT_V1; +#ifdef L9DEBUG + printf("V1 msg table: wordlen=%.2lf", a1); +#endif + } else { + error("\rUnable to identify V1 message table in file: %s\r", filename); + return FALSE; + } + break; + } + case L9_V2: { + double a2, a1; + startmd = startdata + L9WORD(startdata + 0x0); + startmdV2 = startdata + L9WORD(startdata + 0x2); + + /* determine message type */ + if (analyseV2(&a2) && a2 > 2 && a2 < 10) { + L9MsgType = MSGT_V2; +#ifdef L9DEBUG + printf("V2 msg table: wordlen=%.2lf", a2); +#endif + } else if (analyseV1(&a1) && a1 > 2 && a1 < 10) { + L9MsgType = MSGT_V1; +#ifdef L9DEBUG + printf("V1 msg table: wordlen=%.2lf", a1); +#endif + } else { + error("\rUnable to identify V2 message table in file: %s\r", filename); + return FALSE; + } + break; + } + case L9_V3: + case L9_V4: + startmd = startdata + L9WORD(startdata + 0x2); + endmd = startmd + L9WORD(startdata + 0x4); + defdict = startdata + L9WORD(startdata + 6); + endwdp5 = defdict + 5 + L9WORD(startdata + 0x8); + dictdata = startdata + L9WORD(startdata + 0x0a); + dictdatalen = L9WORD(startdata + 0x0c); + wordtable = startdata + L9WORD(startdata + 0xe); + break; + } + +#ifndef NO_SCAN_GRAPHICS + /* If there was no graphics file, look in the game data */ + if (pictureaddress) { + if (!findsubs(pictureaddress, picturesize, &picturedata, &picturesize)) { + picturedata = NULL; + picturesize = 0; + } + } else { + if (!findsubs(startdata, FileSize, &picturedata, &picturesize) + && !findsubs(startfile, startdata - startfile, &picturedata, &picturesize)) { + picturedata = NULL; + picturesize = 0; + } + } +#endif + + memset(FirstLine, 0, FIRSTLINESIZE); + FirstLinePos = 0; + + return TRUE; +} + +L9BOOL checksumgamedata(void) { + return calcchecksum(startdata, L9WORD(startdata) + 1) == 0; +} + +L9UINT16 movewa5d0(void) { + L9UINT16 ret = L9WORD(codeptr); + codeptr += 2; + return ret; +} + +L9UINT16 getcon(void) { + if (code & 64) { + /* getconsmall */ + return *codeptr++; + } else return movewa5d0(); +} + +L9BYTE *getaddr(void) { + if (code & 0x20) { + /* getaddrshort */ + signed char diff = *codeptr++; + return codeptr + diff - 1; + } else { + return acodeptr + movewa5d0(); + } +} + +L9UINT16 *getvar(void) { +#ifndef CODEFOLLOW + return workspace.vartable + *codeptr++; +#else + cfvar2 = cfvar; + return cfvar = workspace.vartable + *codeptr++; +#endif +} + +void Goto(void) { + L9BYTE *target = getaddr(); + if (target == codeptr - 2) + Running = FALSE; /* Endless loop! */ + else + codeptr = target; +} + +void intgosub(void) { + L9BYTE *newcodeptr = getaddr(); + if (workspace.stackptr == STACKSIZE) { + error("\rStack overflow error\r"); + Running = FALSE; + return; + } + workspace.stack[workspace.stackptr++] = (L9UINT16)(codeptr - acodeptr); + codeptr = newcodeptr; +} + +void intreturn(void) { + if (workspace.stackptr == 0) { + error("\rStack underflow error\r"); + Running = FALSE; + return; + } + codeptr = acodeptr + workspace.stack[--workspace.stackptr]; +} + +void printnumber(void) { + printdecimald0(*getvar()); +} + +void messagec(void) { + if (L9GameType <= L9_V2) + printmessageV2(getcon()); + else + printmessage(getcon()); +} + +void messagev(void) { + if (L9GameType <= L9_V2) + printmessageV2(*getvar()); + else + printmessage(*getvar()); +} + +void init(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - init"); +#endif +} + +void randomnumber(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - randomnumber"); +#endif + L9SETWORD(a6, g_vm->getRandomNumber(0xffff)); +} + +void driverclg(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driverclg"); +#endif +} + +void _line(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - line"); +#endif +} + +void fill(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - fill"); +#endif +} + +void driverchgcol(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driverchgcol"); +#endif +} + +void drivercalcchecksum(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - calcchecksum"); +#endif +} + +void driveroswrch(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driveroswrch"); +#endif +} + +void driverosrdch(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driverosrdch"); +#endif + + os_flush(); + if (Cheating) { + *a6 = '\r'; + } else { + /* max delay of 1/50 sec */ + *a6 = os_readchar(20); + } +} + +void driversavefile(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driversavefile"); +#endif +} + +void driverloadfile(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driverloadfile"); +#endif +} + +void settext(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - settext"); +#endif +} + +void resettask(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - resettask"); +#endif +} + +void driverinputline(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - driverinputline"); +#endif +} + +void returntogem(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - returntogem"); +#endif +} + +void lensdisplay(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - lensdisplay"); +#endif + + printstring("\rLenslok code is "); + printchar(*a6); + printchar(*(a6 + 1)); + printchar('\r'); +} + +void allocspace(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - allocspace"); +#endif +} + +void driver14(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - call 14"); +#endif + + *a6 = 0; +} + +void showbitmap(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - showbitmap"); +#endif + + os_show_bitmap(a6[1], a6[3], a6[5]); +} + +void checkfordisc(L9BYTE *a6) { +#ifdef L9DEBUG + printf("driver - checkfordisc"); +#endif + + *a6 = 0; + list9startptr[2] = 0; +} + +void driver(int d0, L9BYTE *a6) { + switch (d0) { + case 0: + init(a6); + break; + case 0x0c: + randomnumber(a6); + break; + case 0x10: + driverclg(a6); + break; + case 0x11: + _line(a6); + break; + case 0x12: + fill(a6); + break; + case 0x13: + driverchgcol(a6); + break; + case 0x01: + drivercalcchecksum(a6); + break; + case 0x02: + driveroswrch(a6); + break; + case 0x03: + driverosrdch(a6); + break; + case 0x05: + driversavefile(a6); + break; + case 0x06: + driverloadfile(a6); + break; + case 0x07: + settext(a6); + break; + case 0x08: + resettask(a6); + break; + case 0x04: + driverinputline(a6); + break; + case 0x09: + returntogem(a6); + break; + /* + case 0x16: ramsave(a6); break; + case 0x17: ramload(a6); break; + */ + case 0x19: + lensdisplay(a6); + break; + case 0x1e: + allocspace(a6); + break; + /* v4 */ + case 0x0e: + driver14(a6); + break; + case 0x20: + showbitmap(a6); + break; + case 0x22: + checkfordisc(a6); + break; + } +} + +void ramsave(int i) { +#ifdef L9DEBUG + printf("driver - ramsave %d", i); +#endif + + memmove(ramsavearea + i, workspace.vartable, sizeof(SaveStruct)); +} + +void ramload(int i) { +#ifdef L9DEBUG + printf("driver - ramload %d", i); +#endif + + memmove(workspace.vartable, ramsavearea + i, sizeof(SaveStruct)); +} + +void calldriver(void) { + L9BYTE *a6 = list9startptr; + int d0 = *a6++; +#ifdef CODEFOLLOW + fprintf(f, " %s", drivercalls[d0]); +#endif + + if (d0 == 0x16 || d0 == 0x17) { + int d1 = *a6; + if (d1 > 0xfa) *a6 = 1; + else if (d1 + 1 >= RAMSAVESLOTS) *a6 = 0xff; + else { + *a6 = 0; + if (d0 == 0x16) ramsave(d1 + 1); + else ramload(d1 + 1); + } + *list9startptr = *a6; + } else if (d0 == 0x0b) { + char NewName[MAX_PATH]; + strcpy(NewName, LastGame); + if (*a6 == 0) { + printstring("\rSearching for next sub-game file.\r"); + if (!os_get_game_file(NewName, MAX_PATH)) { + printstring("\rFailed to load game.\r"); + return; + } + } else { + os_set_filenumber(NewName, MAX_PATH, *a6); + } + LoadGame2(NewName, NULL); + } else driver(d0, a6); +} + +void L9Random(void) { +#ifdef CODEFOLLOW + fprintf(f, " %d", randomseed); +#endif + randomseed = (((randomseed << 8) + 0x0a - randomseed) << 2) + randomseed + 1; + *getvar() = randomseed & 0xff; +#ifdef CODEFOLLOW + fprintf(f, " %d", randomseed); +#endif +} + +void save(void) { + L9UINT16 checksum; + int i; +#ifdef L9DEBUG + printf("function - save"); +#endif + /* does a full save, workpace, stack, codeptr, stackptr, game name, checksum */ + + workspace.Id = L9_ID; + workspace.codeptr = codeptr - acodeptr; + workspace.listsize = LISTAREASIZE; + workspace.stacksize = STACKSIZE; + workspace.filenamesize = MAX_PATH; + workspace.checksum = 0; + strcpy(workspace.filename, LastGame); + + checksum = 0; + for (i = 0; i < (int)sizeof(GameState); i++) checksum += ((L9BYTE *) &workspace)[i]; + workspace.checksum = checksum; + + if (os_save_file((L9BYTE *) &workspace, sizeof(workspace))) printstring("\rGame saved.\r"); + else printstring("\rUnable to save game.\r"); +} + +L9BOOL CheckFile(GameState *gs) { + L9UINT16 checksum; + int i; + char c = 'Y'; + + if (gs->Id != L9_ID) return FALSE; + checksum = gs->checksum; + gs->checksum = 0; + for (i = 0; i < (int)sizeof(GameState); i++) + checksum -= *((L9BYTE *) gs + i); + if (checksum) return FALSE; + if (scumm_stricmp(gs->filename, LastGame)) { + printstring("\rWarning: game path name does not match, you may be about to load this position file into the wrong story file.\r"); + printstring("Are you sure you want to restore? (Y/N)"); + os_flush(); + + c = '\0'; + while ((c != 'y') && (c != 'Y') && (c != 'n') && (c != 'N')) + c = os_readchar(20); + } + if ((c == 'y') || (c == 'Y')) + return TRUE; + return FALSE; +} + +void NormalRestore(void) { + GameState temp; + int Bytes; +#ifdef L9DEBUG + printf("function - restore"); +#endif + if (Cheating) { + /* not really an error */ + Cheating = FALSE; + error("\rWord is: %s\r", ibuff); + } + + if (os_load_file((L9BYTE *) &temp, &Bytes, sizeof(GameState))) { + if (Bytes == V1FILESIZE) { + printstring("\rGame restored.\r"); + memset(workspace.listarea, 0, LISTAREASIZE); + memmove(workspace.vartable, &temp, V1FILESIZE); + } else if (CheckFile(&temp)) { + printstring("\rGame restored.\r"); + /* only copy in workspace */ + memmove(workspace.vartable, temp.vartable, sizeof(SaveStruct)); + } else { + printstring("\rSorry, unrecognised format. Unable to restore\r"); + } + } else printstring("\rUnable to restore game.\r"); +} + +void restore(void) { + int Bytes; + GameState temp; + if (os_load_file((L9BYTE *) &temp, &Bytes, sizeof(GameState))) { + if (Bytes == V1FILESIZE) { + printstring("\rGame restored.\r"); + /* only copy in workspace */ + memset(workspace.listarea, 0, LISTAREASIZE); + memmove(workspace.vartable, &temp, V1FILESIZE); + } else if (CheckFile(&temp)) { + printstring("\rGame restored.\r"); + /* full restore */ + memmove(&workspace, &temp, sizeof(GameState)); + codeptr = acodeptr + workspace.codeptr; + } else { + printstring("\rSorry, unrecognised format. Unable to restore\r"); + } + } else printstring("\rUnable to restore game.\r"); +} + +void playback(void) { + if (scriptfile) + delete scriptfile; + scriptfile = os_open_script_file(); + + if (scriptfile) + printstring("\rPlaying back input from script file.\r"); + else + printstring("\rUnable to play back script file.\r"); +} + +void l9_fgets(char *s, int n, Common::SeekableReadStream *f) { + int c = '\0'; + int count = 0; + + while ((c != '\n') && (c != '\r') && (c != EOF) && (count < n - 1)) { + c = f->readByte(); + *s++ = c; + count++; + } + *s = '\0'; + + if (c == EOF) { + s--; + *s = '\n'; + } else if (c == '\r') { + s--; + *s = '\n'; + + c = f->readByte(); + if ((c != '\r') && (c != EOF)) + f->seek(-1, SEEK_CUR); + } +} + +L9BOOL scriptinput(char *buffer, int size) { + while (scriptfile != NULL) { + if (scriptfile->eos()) { + delete scriptfile; + scriptfile = NULL; + } else { + char *p = buffer; + *p = '\0'; + l9_fgets(buffer, size, scriptfile); + while (*p != '\0') { + switch (*p) { + case '\n': + case '\r': + case '[': + case ';': + *p = '\0'; + break; + case '#': + if ((p == buffer) && (scumm_strnicmp(p, "#seed ", 6) == 0)) + p++; + else + *p = '\0'; + break; + default: + p++; + break; + } + } + if (*buffer != '\0') { + printstring(buffer); + lastchar = lastactualchar = '.'; + return TRUE; + } + } + } + return FALSE; +} + +void clearworkspace(void) { + memset(workspace.vartable, 0, sizeof(workspace.vartable)); +} + +void ilins(int d0) { + error("\rIllegal instruction: %d\r", d0); + Running = FALSE; +} + +void function(void) { + int d0 = *codeptr++; +#ifdef CODEFOLLOW + fprintf(f, " %s", d0 == 250 ? "printstr" : functions[d0 - 1]); +#endif + + switch (d0) { + case 1: + if (L9GameType == L9_V1) + StopGame(); + else + calldriver(); + break; + case 2: + L9Random(); + break; + case 3: + save(); + break; + case 4: + NormalRestore(); + break; + case 5: + clearworkspace(); + break; + case 6: + workspace.stackptr = 0; + break; + case 250: + printstring((char *) codeptr); + while (*codeptr++); + break; + + default: + ilins(d0); + } +} + +void findmsgequiv(int d7) { + int d4 = -1, d0; + L9BYTE *a2 = startmd; + + do { + d4++; + if (a2 > endmd) return; + d0 = *a2; + if (d0 & 0x80) { + a2++; + d4 += d0 & 0x7f; + } else if (d0 & 0x40) { + int d6 = getmdlength(&a2); + do { + int d1; + if (d6 == 0) break; + + d1 = *a2++; + d6--; + if (d1 & 0x80) { + if (d1 < 0x90) { + a2++; + d6--; + } else { + d0 = (d1 << 8) + *a2++; + d6--; + if (d7 == (d0 & 0xfff)) { + d0 = ((d0 << 1) & 0xe000) | d4; + list9ptr[1] = d0; + list9ptr[0] = d0 >> 8; + list9ptr += 2; + if (list9ptr >= list9startptr + 0x20) return; + } + } + } + } while (TRUE); + } else { + int len = getmdlength(&a2); + a2 += len; + } + } while (TRUE); +} + +L9BOOL unpackword(void) { + L9BYTE *a3; + + if (unpackd3 == 0x1b) return TRUE; + + a3 = (L9BYTE *) threechars + (unpackd3 & 3); + + /*uw01 */ + while (TRUE) { + L9BYTE d0 = getdictionarycode(); + if (dictptr >= endwdp5) return TRUE; + if (d0 >= 0x1b) { + *a3 = 0; + unpackd3 = d0; + return FALSE; + } + *a3++ = getdictionary(d0); + } +} + +L9BOOL initunpack(L9BYTE *ptr) { + initdict(ptr); + unpackd3 = 0x1c; + return unpackword(); +} + +int partword(char c) { + c = tolower(c); + + if (c == 0x27 || c == 0x2d) return 0; + if (c < 0x30) return 1; + if (c < 0x3a) return 0; + if (c < 0x61) return 1; + if (c < 0x7b) return 0; + return 1; +} + +L9UINT32 readdecimal(char *buff) { + return atol(buff); +} + +void checknumber(void) { + if (*obuff >= 0x30 && *obuff < 0x3a) { + if (L9GameType == L9_V4) { + *list9ptr = 1; + L9SETWORD(list9ptr + 1, readdecimal(obuff)); + L9SETWORD(list9ptr + 3, 0); + } else { + L9SETDWORD(list9ptr, readdecimal(obuff)); + L9SETWORD(list9ptr + 4, 0); + } + } else { + L9SETWORD(list9ptr, 0x8000); + L9SETWORD(list9ptr + 2, 0); + } +} + +void NextCheat(void) { + /* restore game status */ + memmove(&workspace, &CheatWorkspace, sizeof(GameState)); + codeptr = acodeptr + workspace.codeptr; + + if (!((L9GameType <= L9_V2) ? GetWordV2(ibuff, CheatWord++) : GetWordV3(ibuff, CheatWord++))) { + Cheating = FALSE; + printstring("\rCheat failed.\r"); + *ibuff = 0; + } +} + +void StartCheat(void) { + Cheating = TRUE; + CheatWord = 0; + + /* save current game status */ + memmove(&CheatWorkspace, &workspace, sizeof(GameState)); + CheatWorkspace.codeptr = codeptr - acodeptr; + + NextCheat(); +} + +/* v3,4 input routine */ + +L9BOOL GetWordV3(char *buff, int Word) { + int i; + int subdict = 0; + /* 26*4-1=103 */ + + initunpack(startdata + L9WORD(dictdata)); + unpackword(); + + while (Word--) { + if (unpackword()) { + if (++subdict == dictdatalen) return FALSE; + initunpack(startdata + L9WORD(dictdata + (subdict << 2))); + Word++; /* force unpack again */ + } + } + strcpy(buff, threechars); + for (i = 0; i < (int)strlen(buff); i++) buff[i] &= 0x7f; + return TRUE; +} + +L9BOOL CheckHash(void) { + if (scumm_stricmp(ibuff, "#cheat") == 0) StartCheat(); + else if (scumm_stricmp(ibuff, "#save") == 0) { + save(); + return TRUE; + } else if (scumm_stricmp(ibuff, "#restore") == 0) { + restore(); + return TRUE; + } else if (scumm_stricmp(ibuff, "#quit") == 0) { + StopGame(); + printstring("\rGame Terminated\r"); + return TRUE; + } else if (scumm_stricmp(ibuff, "#dictionary") == 0) { + CheatWord = 0; + printstring("\r"); + while ((L9GameType <= L9_V2) ? GetWordV2(ibuff, CheatWord++) : GetWordV3(ibuff, CheatWord++)) { + error("%s ", ibuff); + if (os_stoplist() || !Running) break; + } + printstring("\r"); + return TRUE; + } else if (scumm_strnicmp(ibuff, "#picture ", 9) == 0) { + int pic = 0; + if (sscanf(ibuff + 9, "%d", &pic) == 1) { + if (L9GameType == L9_V4) + os_show_bitmap(pic, 0, 0); + else + show_picture(pic); + } + + lastactualchar = 0; + printchar('\r'); + return TRUE; + } else if (scumm_strnicmp(ibuff, "#seed ", 6) == 0) { + int seed = 0; + if (sscanf(ibuff + 6, "%d", &seed) == 1) + randomseed = constseed = seed; + lastactualchar = 0; + printchar('\r'); + return TRUE; + } else if (scumm_stricmp(ibuff, "#play") == 0) { + playback(); + return TRUE; + } + return FALSE; +} + +L9BOOL IsInputChar(char c) { + if (c == '-' || c == '\'') + return TRUE; + if ((L9GameType >= L9_V3) && (c == '.' || c == ',')) + return TRUE; + return Common::isAlnum(c); +} + +L9BOOL corruptinginput(void) { + L9BYTE *a0, *a2, *a6; + int d0, d1, d2, keywordnumber, abrevword; + char *iptr; + + list9ptr = list9startptr; + + if (ibuffptr == NULL) { + if (Cheating) NextCheat(); + else { + /* flush */ + os_flush(); + lastchar = lastactualchar = '.'; + /* get input */ + if (!scriptinput(ibuff, IBUFFSIZE)) { + if (!os_input(ibuff, IBUFFSIZE)) + return FALSE; /* fall through */ + } + if (CheckHash()) + return FALSE; + + /* check for invalid chars */ + for (iptr = ibuff; *iptr != 0; iptr++) { + if (!IsInputChar(*iptr)) + *iptr = ' '; + } + + /* force CR but prevent others */ + os_printchar(lastactualchar = '\r'); + } + ibuffptr = (L9BYTE *) ibuff; + } + + a2 = (L9BYTE *) obuff; + a6 = ibuffptr; + + /*ip05 */ + while (TRUE) { + d0 = *a6++; + if (d0 == 0) { + ibuffptr = NULL; + L9SETWORD(list9ptr, 0); + return TRUE; + } + if (partword((char)d0) == 0) break; + if (d0 != 0x20) { + ibuffptr = a6; + L9SETWORD(list9ptr, 0); + L9SETWORD(list9ptr + 2, 0); + list9ptr[1] = d0; + *a2 = 0x20; + keywordnumber = -1; + return TRUE; + } + } + + a6--; + /*ip06loop */ + do { + d0 = *a6++; + if (partword((char)d0) == 1) break; + d0 = tolower(d0); + *a2++ = d0; + } while (a2 < (L9BYTE *) obuff + 0x1f); + /*ip06a */ + *a2 = 0x20; + a6--; + ibuffptr = a6; + abrevword = -1; + keywordnumber = -1; + list9ptr = list9startptr; + /* setindex */ + a0 = dictdata; + d2 = dictdatalen; + d0 = *obuff - 0x61; + if (d0 < 0) { + a6 = defdict; + d1 = 0; + } else { + /*ip10 */ + d1 = 0x67; + if (d0 < 0x1a) { + d1 = d0 << 2; + d0 = obuff[1]; + if (d0 != 0x20) d1 += ((d0 - 0x61) >> 3) & 3; + } + /*ip13 */ + if (d1 >= d2) { + checknumber(); + return TRUE; + } + a0 += d1 << 2; + a6 = startdata + L9WORD(a0); + d1 = L9WORD(a0 + 2); + } + /*ip13gotwordnumber */ + + initunpack(a6); + /*ip14 */ + d1--; + do { + d1++; + if (unpackword()) { + /* ip21b */ + if (abrevword == -1) break; /* goto ip22 */ + else d0 = abrevword; /* goto ip18b */ + } else { + L9BYTE *a1 = (L9BYTE *) threechars; + int d6 = -1; + + a0 = (L9BYTE *) obuff; + /*ip15 */ + do { + d6++; + d0 = tolower(*a1++ & 0x7f); + d2 = *a0++; + } while (d0 == d2); + + if (d2 != 0x20) { + /* ip17 */ + if (abrevword == -1) continue; + else d0 = -1; + } else if (d0 == 0) d0 = d1; + else if (abrevword != -1) break; + else if (d6 >= 4) d0 = d1; + else { + abrevword = d1; + continue; + } + } + /*ip18b */ + findmsgequiv(d1); + + abrevword = -1; + if (list9ptr != list9startptr) { + L9SETWORD(list9ptr, 0); + return TRUE; + } + } while (TRUE); + /* ip22 */ + checknumber(); + return TRUE; +} + +/* version 2 stuff hacked from bbc v2 files */ + +L9BOOL IsDictionaryChar(char c) { + switch (c) { + case '?': + case '-': + case '\'': + case '/': + return TRUE; + case '!': + case '.': + case ',': + return TRUE; + } + return Common::isUpper(c) || Common::isDigit(c); +} + +L9BOOL GetWordV2(char *buff, int Word) { + L9BYTE *ptr = dictdata, x; + + while (Word--) { + do { + x = *ptr++; + } while (x > 0 && x < 0x7f); + if (x == 0) return FALSE; /* no more words */ + ptr++; + } + do { + x = *ptr++; + if (!IsDictionaryChar(x & 0x7f)) return FALSE; + *buff++ = x & 0x7f; + } while (x > 0 && x < 0x7f); + *buff = 0; + return TRUE; +} + +L9BOOL inputV2(int *wordcount) { + L9BYTE a, x; + L9BYTE *ibuffp, *obuffptr, *ptr, *list0ptr; + char *iptr; + + if (Cheating) NextCheat(); + else { + os_flush(); + lastchar = lastactualchar = '.'; + /* get input */ + if (!scriptinput(ibuff, IBUFFSIZE)) { + if (!os_input(ibuff, IBUFFSIZE)) + return FALSE; /* fall through */ + } + if (CheckHash()) + return FALSE; + + /* check for invalid chars */ + for (iptr = ibuff; *iptr != 0; iptr++) { + if (!IsInputChar(*iptr)) + *iptr = ' '; + } + + /* force CR but prevent others */ + os_printchar(lastactualchar = '\r'); + } + /* add space onto end */ + ibuffp = (L9BYTE *) strchr(ibuff, 0); + *ibuffp++ = 32; + *ibuffp = 0; + + *wordcount = 0; + ibuffp = (L9BYTE *) ibuff; + obuffptr = (L9BYTE *) obuff; + /* ibuffp=76,77 */ + /* obuffptr=84,85 */ + /* list0ptr=7c,7d */ + list0ptr = dictdata; + + while (*ibuffp == 32) ++ibuffp; + + ptr = ibuffp; + do { + while (*ptr == 32) ++ptr; + if (*ptr == 0) break; + (*wordcount)++; + do { + a = *++ptr; + } while (a != 32 && a != 0); + } while (*ptr > 0); + + while (TRUE) { + ptr = ibuffp; /* 7a,7b */ + while (*ibuffp == 32) ++ibuffp; + + while (TRUE) { + a = *ibuffp; + x = *list0ptr++; + + if (a == 32) break; + if (a == 0) { + *obuffptr++ = 0; + return TRUE; + } + + ++ibuffp; + if (!IsDictionaryChar(x & 0x7f)) x = 0; + if (tolower(x & 0x7f) != tolower(a)) { + while (x > 0 && x < 0x7f) x = *list0ptr++; + if (x == 0) { + do { + a = *ibuffp++; + if (a == 0) { + *obuffptr = 0; + return TRUE; + } + } while (a != 32); + while (*ibuffp == 32) ++ibuffp; + list0ptr = dictdata; + ptr = ibuffp; + } else { + list0ptr++; + ibuffp = ptr; + } + } else if (x >= 0x7f) break; + } + + a = *ibuffp; + if (a != 32) { + ibuffp = ptr; + list0ptr += 2; + continue; + } + --list0ptr; + while (*list0ptr++ < 0x7e); + *obuffptr++ = *list0ptr; + while (*ibuffp == 32) ++ibuffp; + list0ptr = dictdata; + } +} + +void input(void) { + if (L9GameType == L9_V3 && FirstPicture >= 0) { + show_picture(FirstPicture); + FirstPicture = -1; + } + + /* if corruptinginput() returns false then, input will be called again + next time around instructionloop, this is used when save() and restore() + are called out of line */ + + codeptr--; + if (L9GameType <= L9_V2) { + int wordcount; + if (inputV2(&wordcount)) { + L9BYTE *obuffptr = (L9BYTE *) obuff; + codeptr++; + *getvar() = *obuffptr++; + *getvar() = *obuffptr++; + *getvar() = *obuffptr; + *getvar() = wordcount; + } + } else if (corruptinginput()) codeptr += 5; +} + +void varcon(void) { + L9UINT16 d6 = getcon(); + *getvar() = d6; + +#ifdef CODEFOLLOW + fprintf(f, " Var[%d]=%d)", cfvar - workspace.vartable, *cfvar); +#endif +} + +void varvar(void) { + L9UINT16 d6 = *getvar(); + *getvar() = d6; + +#ifdef CODEFOLLOW + fprintf(f, " Var[%d]=Var[%d] (=%d)", cfvar - workspace.vartable, cfvar2 - workspace.vartable, d6); +#endif +} + +void _add(void) { + L9UINT16 d0 = *getvar(); + *getvar() += d0; + +#ifdef CODEFOLLOW + fprintf(f, " Var[%d]+=Var[%d] (+=%d)", cfvar - workspace.vartable, cfvar2 - workspace.vartable, d0); +#endif +} + +void _sub(void) { + L9UINT16 d0 = *getvar(); + *getvar() -= d0; + +#ifdef CODEFOLLOW + fprintf(f, " Var[%d]-=Var[%d] (-=%d)", cfvar - workspace.vartable, cfvar2 - workspace.vartable, d0); +#endif +} + +void jump(void) { + L9UINT16 d0 = L9WORD(codeptr); + L9BYTE *a0; + codeptr += 2; + + a0 = acodeptr + ((d0 + ((*getvar()) << 1)) & 0xffff); + codeptr = acodeptr + L9WORD(a0); +} + +/* bug */ +void exit1(L9BYTE *d4, L9BYTE *d5p, L9BYTE d6, L9BYTE d7) { + L9BYTE *a0 = absdatablock; + L9BYTE d1 = d7, d0; + if (--d1) { + do { + d0 = *a0; + if (L9GameType == L9_V4) { + if ((d0 == 0) && (*(a0 + 1) == 0)) + goto notfn4; + } + a0 += 2; + } while ((d0 & 0x80) == 0 || --d1); + } + + do { + *d4 = *a0++; + if (((*d4) & 0xf) == d6) { + *d5p = *a0; + return; + } + a0++; + } while (((*d4) & 0x80) == 0); + + /* notfn4 */ +notfn4: + d6 = exitreversaltable[d6]; + a0 = absdatablock; + *d5p = 1; + + do { + *d4 = *a0++; + if (((*d4) & 0x10) == 0 || ((*d4) & 0xf) != d6) a0++; + else if (*a0++ == d7) return; + /* exit6noinc */ + if ((*d4) & 0x80)(*d5p)++; + } while (*d4); + *d5p = 0; +} + +void Exit(void) { + L9BYTE d4, d5v; + L9BYTE d7 = (L9BYTE) * getvar(); + L9BYTE d6 = (L9BYTE) * getvar(); +#ifdef CODEFOLLOW + fprintf(f, " d7=%d d6=%d", d7, d6); +#endif + exit1(&d4, &d5v, d6, d7); + + *getvar() = (d4 & 0x70) >> 4; + *getvar() = d5v; +#ifdef CODEFOLLOW + fprintf(f, " Var[%d]=%d(d4=%d) Var[%d]=%d", + cfvar2 - workspace.vartable, (d4 & 0x70) >> 4, d4, cfvar - workspace.vartable, d5v); +#endif +} + +void ifeqvt(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = *getvar(); + L9BYTE *a0 = getaddr(); + if (d0 == d1) codeptr = a0; + +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]=Var[%d] goto %d (%s)", cfvar2 - workspace.vartable, cfvar - workspace.vartable, (L9UINT32)(a0 - acodeptr), d0 == d1 ? "Yes" : "No"); +#endif +} + +void ifnevt(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = *getvar(); + L9BYTE *a0 = getaddr(); + if (d0 != d1) codeptr = a0; + +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]!=Var[%d] goto %d (%s)", cfvar2 - workspace.vartable, cfvar - workspace.vartable, (L9UINT32)(a0 - acodeptr), d0 != d1 ? "Yes" : "No"); +#endif +} + +void ifltvt(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = *getvar(); + L9BYTE *a0 = getaddr(); + if (d0 < d1) codeptr = a0; + +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]<Var[%d] goto %d (%s)", cfvar2 - workspace.vartable, cfvar - workspace.vartable, (L9UINT32)(a0 - acodeptr), d0 < d1 ? "Yes" : "No"); +#endif +} + +void ifgtvt(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = *getvar(); + L9BYTE *a0 = getaddr(); + if (d0 > d1) codeptr = a0; + +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]>Var[%d] goto %d (%s)", cfvar2 - workspace.vartable, cfvar - workspace.vartable, (L9UINT32)(a0 - acodeptr), d0 > d1 ? "Yes" : "No"); +#endif +} + +int scalex(int x) { + return (gfx_mode != GFX_V3C) ? (x >> 6) : (x >> 5); +} + +int scaley(int y) { + return (gfx_mode == GFX_V2) ? 127 - (y >> 7) : 95 - (((y >> 5) + (y >> 6)) >> 3); +} + +void detect_gfx_mode(void) { + if (L9GameType == L9_V3) { + /* These V3 games use graphics logic similar to the V2 games */ + if (strstr(FirstLine, "price of magik") != 0) + gfx_mode = GFX_V3A; + else if (strstr(FirstLine, "the archers") != 0) + gfx_mode = GFX_V3A; + else if (strstr(FirstLine, "secret diary of adrian mole") != 0) + gfx_mode = GFX_V3A; + else if ((strstr(FirstLine, "worm in paradise") != 0) + && (strstr(FirstLine, "silicon dreams") == 0)) + gfx_mode = GFX_V3A; + else if (strstr(FirstLine, "growing pains of adrian mole") != 0) + gfx_mode = GFX_V3B; + else if (strstr(FirstLine, "jewels of darkness") != 0 && picturesize < 11000) + gfx_mode = GFX_V3B; + else if (strstr(FirstLine, "silicon dreams") != 0) { + if (picturesize > 11000 + || (startdata[0] == 0x14 && startdata[1] == 0x7d) /* Return to Eden /SD (PC) */ + || (startdata[0] == 0xd7 && startdata[1] == 0x7c)) /* Worm in Paradise /SD (PC) */ + gfx_mode = GFX_V3C; + else + gfx_mode = GFX_V3B; + } else + gfx_mode = GFX_V3C; + } else + gfx_mode = GFX_V2; +} + +void _screen(void) { + int mode = 0; + + if (L9GameType == L9_V3 && strlen(FirstLine) == 0) { + if (*codeptr++) + codeptr++; + return; + } + + detect_gfx_mode(); + l9textmode = *codeptr++; + if (l9textmode) { + if (L9GameType == L9_V4) + mode = 2; + else if (picturedata) + mode = 1; + } + os_graphics(mode); + + screencalled = 1; + +#ifdef L9DEBUG + printf("screen %s", l9textmode ? "graphics" : "text"); +#endif + + if (l9textmode) { + codeptr++; + /* clearg */ + /* gintclearg */ + os_cleargraphics(); + + /* title pic */ + if (showtitle == 1 && mode == 2) { + showtitle = 0; + os_show_bitmap(0, 0, 0); + } + } + /* screent */ +} + +void cleartg(void) { + int d0 = *codeptr++; +#ifdef L9DEBUG + printf("cleartg %s", d0 ? "graphics" : "text"); +#endif + + if (d0) { + /* clearg */ + if (l9textmode) + /* gintclearg */ + os_cleargraphics(); + } + /* cleart */ + /* oswrch(0x0c) */ +} + +L9BOOL validgfxptr(L9BYTE *a5) { + return ((a5 >= picturedata) && (a5 < picturedata + picturesize)); +} + +L9BOOL findsub(int d0, L9BYTE **a5) { + int d1, d2, d3, d4; + + d1 = d0 << 4; + d2 = d1 >> 8; + *a5 = picturedata; + /* findsubloop */ + while (TRUE) { + d3 = *(*a5)++; + if (!validgfxptr(*a5)) + return FALSE; + if (d3 & 0x80) + return FALSE; + if (d2 == d3) { + if ((d1 & 0xff) == (*(*a5) & 0xf0)) { + (*a5) += 2; + return TRUE; + } + } + + d3 = *(*a5)++ & 0x0f; + if (!validgfxptr(*a5)) + return FALSE; + + d4 = **a5; + if ((d3 | d4) == 0) + return FALSE; + + (*a5) += (d3 << 8) + d4 - 2; + if (!validgfxptr(*a5)) + return FALSE; + } +} + +void gosubd0(int d0, L9BYTE **a5) { + if (GfxA5StackPos < GFXSTACKSIZE) { + GfxA5Stack[GfxA5StackPos] = *a5; + GfxA5StackPos++; + GfxScaleStack[GfxScaleStackPos] = scale; + GfxScaleStackPos++; + + if (findsub(d0, a5) == FALSE) { + GfxA5StackPos--; + *a5 = GfxA5Stack[GfxA5StackPos]; + GfxScaleStackPos--; + scale = GfxScaleStack[GfxScaleStackPos]; + } + } +} + +void newxy(int x, int y) { + drawx += (x * scale) & ~7; + drawy += (y * scale) & ~7; +} + +/* sdraw instruction plus arguments are stored in an 8 bit word. + 76543210 + iixxxyyy + where i is instruction code + x is x argument, high bit is sign + y is y argument, high bit is sign +*/ +void sdraw(int d7) { + int x, y, x1, y1; + + /* getxy1 */ + x = (d7 & 0x18) >> 3; + if (d7 & 0x20) + x = (x | 0xfc) - 0x100; + y = (d7 & 0x3) << 2; + if (d7 & 0x4) + y = (y | 0xf0) - 0x100; + + if (reflectflag & 2) + x = -x; + if (reflectflag & 1) + y = -y; + + /* gintline */ + x1 = drawx; + y1 = drawy; + newxy(x, y); + +#ifdef L9DEBUG + printf("gfx - sdraw (%d,%d) (%d,%d) colours %d,%d", + x1, y1, drawx, drawy, gintcolour & 3, option & 3); +#endif + + os_drawline(scalex(x1), scaley(y1), scalex(drawx), scaley(drawy), + gintcolour & 3, option & 3); +} + +/* smove instruction plus arguments are stored in an 8 bit word. + 76543210 + iixxxyyy + where i is instruction code + x is x argument, high bit is sign + y is y argument, high bit is sign +*/ +void smove(int d7) { + int x, y; + + /* getxy1 */ + x = (d7 & 0x18) >> 3; + if (d7 & 0x20) + x = (x | 0xfc) - 0x100; + y = (d7 & 0x3) << 2; + if (d7 & 0x4) + y = (y | 0xf0) - 0x100; + + if (reflectflag & 2) + x = -x; + if (reflectflag & 1) + y = -y; + newxy(x, y); +} + +void sgosub(int d7, L9BYTE **a5) { + int d0 = d7 & 0x3f; +#ifdef L9DEBUG + printf("gfx - sgosub 0x%.2x", d0); +#endif + gosubd0(d0, a5); +} + +/* draw instruction plus arguments are stored in a 16 bit word. + FEDCBA9876543210 + iiiiixxxxxxyyyyy + where i is instruction code + x is x argument, high bit is sign + y is y argument, high bit is sign +*/ +void draw(int d7, L9BYTE **a5) { + int xy, x, y, x1, y1; + + /* getxy2 */ + xy = (d7 << 8) + (*(*a5)++); + x = (xy & 0x3e0) >> 5; + if (xy & 0x400) + x = (x | 0xe0) - 0x100; + y = (xy & 0xf) << 2; + if (xy & 0x10) + y = (y | 0xc0) - 0x100; + + if (reflectflag & 2) + x = -x; + if (reflectflag & 1) + y = -y; + + /* gintline */ + x1 = drawx; + y1 = drawy; + newxy(x, y); + +#ifdef L9DEBUG + printf("gfx - draw (%d,%d) (%d,%d) colours %d,%d", + x1, y1, drawx, drawy, gintcolour & 3, option & 3); +#endif + + os_drawline(scalex(x1), scaley(y1), scalex(drawx), scaley(drawy), + gintcolour & 3, option & 3); +} + +/* move instruction plus arguments are stored in a 16 bit word. + FEDCBA9876543210 + iiiiixxxxxxyyyyy + where i is instruction code + x is x argument, high bit is sign + y is y argument, high bit is sign +*/ +void _move(int d7, L9BYTE **a5) { + int xy, x, y; + + /* getxy2 */ + xy = (d7 << 8) + (*(*a5)++); + x = (xy & 0x3e0) >> 5; + if (xy & 0x400) + x = (x | 0xe0) - 0x100; + y = (xy & 0xf) << 2; + if (xy & 0x10) + y = (y | 0xc0) - 0x100; + + if (reflectflag & 2) + x = -x; + if (reflectflag & 1) + y = -y; + newxy(x, y); +} + +void icolour(int d7) { + gintcolour = d7 & 3; +#ifdef L9DEBUG + printf("gfx - icolour 0x%.2x", gintcolour); +#endif +} + +void size(int d7) { + static int sizetable[7] = { 0x02, 0x04, 0x06, 0x07, 0x09, 0x0c, 0x10 }; + + d7 &= 7; + if (d7) { + int d0 = (scale * sizetable[d7 - 1]) >> 3; + scale = (d0 < 0x100) ? d0 : 0xff; + } else { + /* sizereset */ + scale = 0x80; + if (gfx_mode == GFX_V2 || gfx_mode == GFX_V3A) + GfxScaleStackPos = 0; + } + +#ifdef L9DEBUG + printf("gfx - size 0x%.2x", scale); +#endif +} + +void gintfill(int d7) { + if ((d7 & 7) == 0) + /* filla */ + d7 = gintcolour; + else + d7 &= 3; + /* fillb */ + +#ifdef L9DEBUG + printf("gfx - gintfill (%d,%d) colours %d,%d", drawx, drawy, d7 & 3, option & 3); +#endif + + os_fill(scalex(drawx), scaley(drawy), d7 & 3, option & 3); +} + +void gosub(int d7, L9BYTE **a5) { + int d0 = ((d7 & 7) << 8) + (*(*a5)++); +#ifdef L9DEBUG + printf("gfx - gosub 0x%.2x", d0); +#endif + gosubd0(d0, a5); +} + +void reflect(int d7) { +#ifdef L9DEBUG + printf("gfx - reflect 0x%.2x", d7); +#endif + + if (d7 & 4) { + d7 &= 3; + d7 ^= reflectflag; + } + /* reflect1 */ + reflectflag = d7; +} + +void notimp(void) { +#ifdef L9DEBUG + printf("gfx - notimp"); +#endif +} + +void gintchgcol(L9BYTE **a5) { + int d0 = *(*a5)++; + +#ifdef L9DEBUG + printf("gfx - gintchgcol %d %d", (d0 >> 3) & 3, d0 & 7); +#endif + + os_setcolour((d0 >> 3) & 3, d0 & 7); +} + +void amove(L9BYTE **a5) { + drawx = 0x40 * (*(*a5)++); + drawy = 0x40 * (*(*a5)++); +#ifdef L9DEBUG + printf("gfx - amove (%d,%d)", drawx, drawy); +#endif +} + +void opt(L9BYTE **a5) { + int d0 = *(*a5)++; +#ifdef L9DEBUG + printf("gfx - opt 0x%.2x", d0); +#endif + + if (d0) + d0 = (d0 & 3) | 0x80; + /* optend */ + option = d0; +} + +void restorescale(void) { +#ifdef L9DEBUG + printf("gfx - restorescale"); +#endif + if (GfxScaleStackPos > 0) + scale = GfxScaleStack[GfxScaleStackPos - 1]; +} + +L9BOOL rts(L9BYTE **a5) { + if (GfxA5StackPos > 0) { + GfxA5StackPos--; + *a5 = GfxA5Stack[GfxA5StackPos]; + if (GfxScaleStackPos > 0) { + GfxScaleStackPos--; + scale = GfxScaleStack[GfxScaleStackPos]; + } + return TRUE; + } + return FALSE; +} + +L9BOOL getinstruction(L9BYTE **a5) { + int d7 = *(*a5)++; + if ((d7 & 0xc0) != 0xc0) { + switch ((d7 >> 6) & 3) { + case 0: + sdraw(d7); + break; + case 1: + smove(d7); + break; + case 2: + sgosub(d7, a5); + break; + } + } else if ((d7 & 0x38) != 0x38) { + switch ((d7 >> 3) & 7) { + case 0: + draw(d7, a5); + break; + case 1: + _move(d7, a5); + break; + case 2: + icolour(d7); + break; + case 3: + size(d7); + break; + case 4: + gintfill(d7); + break; + case 5: + gosub(d7, a5); + break; + case 6: + reflect(d7); + break; + } + } else { + switch (d7 & 7) { + case 0: + notimp(); + break; + case 1: + gintchgcol(a5); + break; + case 2: + notimp(); + break; + case 3: + amove(a5); + break; + case 4: + opt(a5); + break; + case 5: + restorescale(); + break; + case 6: + notimp(); + break; + case 7: + return rts(a5); + } + } + return TRUE; +} + +void absrunsub(int d0) { + L9BYTE *a5; + if (!findsub(d0, &a5)) + return; + while (getinstruction(&a5)); +} + +void show_picture(int pic) { + if (L9GameType == L9_V3 && strlen(FirstLine) == 0) { + FirstPicture = pic; + return; + } + + if (picturedata) { + /* Some games don't call the screen() opcode before drawing + graphics, so here graphics are enabled if necessary. */ + if ((screencalled == 0) && (l9textmode == 0)) { + detect_gfx_mode(); + l9textmode = 1; + os_graphics(1); + } + +#ifdef L9DEBUG + printf("picture %d", pic); +#endif + + os_cleargraphics(); + /* gintinit */ + gintcolour = 3; + option = 0x80; + reflectflag = 0; + drawx = 0x1400; + drawy = 0x1400; + /* sizereset */ + scale = 0x80; + + GfxA5StackPos = 0; + GfxScaleStackPos = 0; + absrunsub(0); + if (!findsub(pic, &gfxa5)) + gfxa5 = NULL; + } +} + +void picture(void) { + show_picture(*getvar()); +} + +void GetPictureSize(int *width, int *height) { + if (L9GameType == L9_V4) { + if (width != NULL) + *width = 0; + if (height != NULL) + *height = 0; + } else { + if (width != NULL) + *width = (gfx_mode != GFX_V3C) ? 160 : 320; + if (height != NULL) + *height = (gfx_mode == GFX_V2) ? 128 : 96; + } +} + +L9BOOL RunGraphics(void) { + if (gfxa5) { + if (!getinstruction(&gfxa5)) + gfxa5 = NULL; + return TRUE; + } + return FALSE; +} + +void initgetobj(void) { + int i; + numobjectfound = 0; + object = 0; + for (i = 0; i < 32; i++) gnoscratch[i] = 0; +} + +void getnextobject(void) { + int d2, d3, d4; + L9UINT16 *hisearchposvar, *searchposvar; + +#ifdef L9DEBUG + printf("getnextobject"); +#endif + + d2 = *getvar(); + hisearchposvar = getvar(); + searchposvar = getvar(); + d3 = *hisearchposvar; + d4 = *searchposvar; + + /* gnoabs */ + do { + if ((d3 | d4) == 0) { + /* initgetobjsp */ + gnosp = 128; + searchdepth = 0; + initgetobj(); + break; + } + + if (numobjectfound == 0) inithisearchpos = d3; + + /* gnonext */ + do { + if (d4 == list2ptr[++object]) { + /* gnomaybefound */ + int d6 = list3ptr[object] & 0x1f; + if (d6 != d3) { + if (d6 == 0 || d3 == 0) continue; + if (d3 != 0x1f) { + gnoscratch[d6] = d6; + continue; + } + d3 = d6; + } + /* gnofound */ + numobjectfound++; + gnostack[--gnosp] = object; + gnostack[--gnosp] = 0x1f; + + *hisearchposvar = d3; + *searchposvar = d4; + *getvar() = object; + *getvar() = numobjectfound; + *getvar() = searchdepth; + return; + } + } while (object <= d2); + + if (inithisearchpos == 0x1f) { + gnoscratch[d3] = 0; + d3 = 0; + + /* gnoloop */ + do { + if (gnoscratch[d3]) { + gnostack[--gnosp] = d4; + gnostack[--gnosp] = d3; + } + } while (++d3 < 0x1f); + } + /* gnonewlevel */ + if (gnosp != 128) { + d3 = gnostack[gnosp++]; + d4 = gnostack[gnosp++]; + } else d3 = d4 = 0; + + numobjectfound = 0; + if (d3 == 0x1f) searchdepth++; + + initgetobj(); + } while (d4); + + /* gnofinish */ + /* gnoreturnargs */ + *hisearchposvar = 0; + *searchposvar = 0; + *getvar() = object = 0; + *getvar() = numobjectfound; + *getvar() = searchdepth; +} + +void ifeqct(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = getcon(); + L9BYTE *a0 = getaddr(); + if (d0 == d1) codeptr = a0; +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]=%d goto %d (%s)", cfvar - workspace.vartable, d1, (L9UINT32)(a0 - acodeptr), d0 == d1 ? "Yes" : "No"); +#endif +} + +void ifnect(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = getcon(); + L9BYTE *a0 = getaddr(); + if (d0 != d1) codeptr = a0; +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]!=%d goto %d (%s)", cfvar - workspace.vartable, d1, (L9UINT32)(a0 - acodeptr), d0 != d1 ? "Yes" : "No"); +#endif +} + +void ifltct(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = getcon(); + L9BYTE *a0 = getaddr(); + if (d0 < d1) codeptr = a0; +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]<%d goto %d (%s)", cfvar - workspace.vartable, d1, (L9UINT32)(a0 - acodeptr), d0 < d1 ? "Yes" : "No"); +#endif +} + +void ifgtct(void) { + L9UINT16 d0 = *getvar(); + L9UINT16 d1 = getcon(); + L9BYTE *a0 = getaddr(); + if (d0 > d1) codeptr = a0; +#ifdef CODEFOLLOW + fprintf(f, " if Var[%d]>%d goto %d (%s)", cfvar - workspace.vartable, d1, (L9UINT32)(a0 - acodeptr), d0 > d1 ? "Yes" : "No"); +#endif +} + +void printinput(void) { + L9BYTE *ptr = (L9BYTE *) obuff; + char c; + while ((c = *ptr++) != ' ') printchar(c); + +#ifdef L9DEBUG + printf("printinput"); +#endif +} + +void listhandler(void) { + L9BYTE *a4, *MinAccess, *MaxAccess; + L9UINT16 val; + L9UINT16 *var; +#ifdef CODEFOLLOW + int offset; +#endif + + if ((code & 0x1f) > 0xa) { + error("\rillegal list access %d\r", code & 0x1f); + Running = FALSE; + return; + } + a4 = L9Pointers[1 + (code & 0x1f)]; + + if (a4 >= workspace.listarea && a4 < workspace.listarea + LISTAREASIZE) { + MinAccess = workspace.listarea; + MaxAccess = workspace.listarea + LISTAREASIZE; + } else { + MinAccess = startdata; + MaxAccess = startdata + FileSize; + } + + if (code >= 0xe0) { + /* listvv */ +#ifndef CODEFOLLOW + a4 += *getvar(); + val = *getvar(); +#else + offset = *getvar(); + a4 += offset; + var = getvar(); + val = *var; + fprintf(f, " list %d [%d]=Var[%d] (=%d)", code & 0x1f, offset, var - workspace.vartable, val); +#endif + + if (a4 >= MinAccess && a4 < MaxAccess) *a4 = (L9BYTE) val; +#ifdef L9DEBUG + else printf("Out of range list access"); +#endif + } else if (code >= 0xc0) { + /* listv1c */ +#ifndef CODEFOLLOW + a4 += *codeptr++; + var = getvar(); +#else + offset = *codeptr++; + a4 += offset; + var = getvar(); + fprintf(f, " Var[%d]= list %d [%d])", var - workspace.vartable, code & 0x1f, offset); + if (a4 >= MinAccess && a4 < MaxAccess) fprintf(f, " (=%d)", *a4); +#endif + + if (a4 >= MinAccess && a4 < MaxAccess) *var = *a4; + else { + *var = 0; +#ifdef L9DEBUG + printf("Out of range list access"); +#endif + } + } else if (code >= 0xa0) { + /* listv1v */ +#ifndef CODEFOLLOW + a4 += *getvar(); + var = getvar(); +#else + offset = *getvar(); + a4 += offset; + var = getvar(); + + fprintf(f, " Var[%d] =list %d [%d]", var - workspace.vartable, code & 0x1f, offset); + if (a4 >= MinAccess && a4 < MaxAccess) fprintf(f, " (=%d)", *a4); +#endif + + if (a4 >= MinAccess && a4 < MaxAccess) *var = *a4; + else { + *var = 0; +#ifdef L9DEBUG + printf("Out of range list access"); +#endif + } + } else { +#ifndef CODEFOLLOW + a4 += *codeptr++; + val = *getvar(); +#else + offset = *codeptr++; + a4 += offset; + var = getvar(); + val = *var; + fprintf(f, " list %d [%d]=Var[%d] (=%d)", code & 0x1f, offset, var - workspace.vartable, val); +#endif + + if (a4 >= MinAccess && a4 < MaxAccess) *a4 = (L9BYTE) val; +#ifdef L9DEBUG + else printf("Out of range list access"); +#endif + } +} + +void executeinstruction(void) { +#ifdef CODEFOLLOW + f = fopen(CODEFOLLOWFILE, "a"); + fprintf(f, "%ld (s:%d) %x", (L9UINT32)(codeptr - acodeptr) - 1, workspace.stackptr, code); + if (!(code & 0x80)) + fprintf(f, " = %s", codes[code & 0x1f]); +#endif + + if (code & 0x80) + listhandler(); + else { + switch (code & 0x1f) { + case 0: + Goto(); + break; + case 1: + intgosub(); + break; + case 2: + intreturn(); + break; + case 3: + printnumber(); + break; + case 4: + messagev(); + break; + case 5: + messagec(); + break; + case 6: + function(); + break; + case 7: + input(); + break; + case 8: + varcon(); + break; + case 9: + varvar(); + break; + case 10: + _add(); + break; + case 11: + _sub(); + break; + case 12: + ilins(code & 0x1f); + break; + case 13: + ilins(code & 0x1f); + break; + case 14: + jump(); + break; + case 15: + Exit(); + break; + case 16: + ifeqvt(); + break; + case 17: + ifnevt(); + break; + case 18: + ifltvt(); + break; + case 19: + ifgtvt(); + break; + case 20: + _screen(); + break; + case 21: + cleartg(); + break; + case 22: + picture(); + break; + case 23: + getnextobject(); + break; + case 24: + ifeqct(); + break; + case 25: + ifnect(); + break; + case 26: + ifltct(); + break; + case 27: + ifgtct(); + break; + case 28: + printinput(); + break; + case 29: + ilins(code & 0x1f); + break; + case 30: + ilins(code & 0x1f); + break; + case 31: + ilins(code & 0x1f); + break; + } + } +#ifdef CODEFOLLOW + fprintf(f, "\n"); + f.close(); +#endif +} + +L9BOOL LoadGame2(const char *filename, char *picname) { +#ifdef CODEFOLLOW + f = fopen(CODEFOLLOWFILE, "w"); + fprintf(f, "Code follow file...\n"); + f.close(); +#endif + + /* may be already running a game, maybe in input routine */ + Running = FALSE; + ibuffptr = NULL; + + /* intstart */ + if (!intinitialise(filename, picname)) return FALSE; + /* if (!checksumgamedata()) return FALSE; */ + + codeptr = acodeptr; + if (constseed > 0) + randomseed = constseed; + else + randomseed = (L9UINT16)g_system->getMillis(); + strcpy(LastGame, filename); + return Running = TRUE; +} + +L9BOOL LoadGame(const char *filename, char *picname) { + L9BOOL ret = LoadGame2(filename, picname); + showtitle = 1; + clearworkspace(); + workspace.stackptr = 0; + /* need to clear listarea as well */ + memset((L9BYTE *) workspace.listarea, 0, LISTAREASIZE); + return ret; +} + +/* can be called from input to cause fall through for exit */ +void StopGame(void) { + Running = FALSE; +} + +L9BOOL RunGame(void) { + code = *codeptr++; + /* printf("%d",code); */ + executeinstruction(); + return Running; +} + +void RestoreGame(char *filename) { + int Bytes; + GameState temp; + Common::File f; + + if (f.open(filename)) { + // TODO: This is horribly unportable + Bytes = f.read(&temp, sizeof(GameState)); + if (Bytes == V1FILESIZE) { + printstring("\rGame restored.\r"); + /* only copy in workspace */ + memset(workspace.listarea, 0, LISTAREASIZE); + memmove(workspace.vartable, &temp, V1FILESIZE); + } else if (CheckFile(&temp)) { + printstring("\rGame restored.\r"); + /* full restore */ + memmove(&workspace, &temp, sizeof(GameState)); + codeptr = acodeptr + workspace.codeptr; + } else + printstring("\rSorry, unrecognised format. Unable to restore\r"); + } else + printstring("\rUnable to restore game.\r"); +} + +} // End of namespace Level9 +} // End of namespace Glk diff --git a/engines/glk/level9/level9_main.h b/engines/glk/level9/level9_main.h new file mode 100644 index 0000000000..ab30242c07 --- /dev/null +++ b/engines/glk/level9/level9_main.h @@ -0,0 +1,123 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef GLK_LEVEL9_LEVEL9_MAIN +#define GLK_LEVEL9_LEVEL9_MAIN + +#include "common/scummsys.h" +#include "common/endian.h" +#include "common/stream.h" + +namespace Glk { +namespace Level9 { + +typedef byte L9BYTE; +typedef uint16 L9UINT16; +typedef uint32 L9UINT32; +typedef bool L9BOOL; + +#define FALSE false +#define TRUE true + +#define LISTAREASIZE 0x800 +#define STACKSIZE 1024 +#define V1FILESIZE 0x600 + +#ifndef MAX_PATH +#define MAX_PATH 256 +#endif + +struct GameState { + L9UINT32 Id; + L9UINT16 codeptr, stackptr, listsize, stacksize, filenamesize, checksum; + L9UINT16 vartable[256]; + L9BYTE listarea[LISTAREASIZE]; + L9UINT16 stack[STACKSIZE]; + char filename[MAX_PATH]; +}; + +enum BitmapType { + NO_BITMAPS, + AMIGA_BITMAPS, + PC1_BITMAPS, + PC2_BITMAPS, + C64_BITMAPS, + BBC_BITMAPS, + CPC_BITMAPS, + MAC_BITMAPS, + ST1_BITMAPS, + ST2_BITMAPS +}; + +struct Colour { + L9BYTE red, green, blue; +}; + +struct Bitmap { + L9UINT16 width, height; + L9BYTE *bitmap; + Colour palette[32]; + L9UINT16 npalette; +}; + +#define MAX_BITMAP_WIDTH 512 +#define MAX_BITMAP_HEIGHT 218 + +#define L9WORD(x) READ_LE_UINT16(x) +#define L9SETWORD(x,val) WRITE_LE_UINT16(x, val) +#define L9SETDWORD(x,val) WRITE_LE_UINT32(x, val) + +/* routines provided by os dependent code */ +void os_printchar(char c); +L9BOOL os_input(char *ibuff, int size); +char os_readchar(int millis); +L9BOOL os_stoplist(void); +void os_flush(void); +L9BOOL os_save_file(L9BYTE *Ptr, int Bytes); +L9BOOL os_load_file(L9BYTE *Ptr, int *Bytes, int Max); +L9BOOL os_get_game_file(char *NewName, int Size); +void os_set_filenumber(char *NewName, int Size, int n); +void os_graphics(int mode); +void os_cleargraphics(void); +void os_setcolour(int colour, int index); +void os_drawline(int x1, int y1, int x2, int y2, int colour1, int colour2); +void os_fill(int x, int y, int colour1, int colour2); +void os_show_bitmap(int pic, int x, int y); +Common::SeekableReadStream *os_open_script_file(void); + +/* routines provided by level9 interpreter */ +L9BOOL LoadGame(const char *filename, char *picname); +L9BOOL RunGame(void); +void StopGame(void); +void RestoreGame(char *filename); +void FreeMemory(void); +void GetPictureSize(int *width, int *height); +L9BOOL RunGraphics(void); + +/* bitmap routines provided by level9 interpreter */ +BitmapType DetectBitmaps(char *dir); +Bitmap *DecodeBitmap(char *dir, BitmapType type, int num, int x, int y); + +} // End of namespace Level9 +} // End of namespace Glk + +#endif diff --git a/engines/glk/level9/os_glk.cpp b/engines/glk/level9/os_glk.cpp new file mode 100644 index 0000000000..ef5499c0f0 --- /dev/null +++ b/engines/glk/level9/os_glk.cpp @@ -0,0 +1,6084 @@ +/* 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/level9/level9_main.h" +#include "glk/level9/level9.h" +#include "common/textconsole.h" + +namespace Glk { +namespace Level9 { + +#define BYTE_MAX 0xff +#define BITS_PER_CHAR 8 + +/* File path delimiter, used to be #defined in v2 interpreter. */ +#if defined(_Windows) || defined(__MSDOS__) \ + || defined (_WIN32) || defined (__WIN32__) +static const char GLN_FILE_DELIM = '\\'; +#else +static const char GLN_FILE_DELIM = '/'; +#endif + +/*---------------------------------------------------------------------*/ +/* Module variables, miscellaneous other stuff */ +/*---------------------------------------------------------------------*/ + +/* Glk Level 9 port version number. */ +static const glui32 GLN_PORT_VERSION = 0x00020201; + +/* + * We use a maximum of three Glk windows, one for status, one for pictures, + * and one for everything else. The status and pictures windows may be + * NULL, depending on user selections and the capabilities of the Glk + * library. + */ +static winid_t gln_main_window = NULL, + gln_status_window = NULL, + gln_graphics_window = NULL; + +/* + * Transcript stream and input log. These are NULL if there is no current + * collection of these strings. + */ +static strid_t gln_transcript_stream = NULL, + gln_inputlog_stream = NULL; + +/* Input read log stream, for reading back an input log. */ +static strid_t gln_readlog_stream = NULL; + +/* Note about whether graphics is possible, or not. */ +static int gln_graphics_possible = TRUE; + +/* Options that may be turned off by command line flags. */ +static int gln_graphics_enabled = TRUE, + gln_intercept_enabled = TRUE, + gln_prompt_enabled = TRUE, + gln_loopcheck_enabled = TRUE, + gln_abbreviations_enabled = TRUE, + gln_commands_enabled = TRUE; + +/* Reason for stopping the game, used to detect restarts and ^C exits. */ +enum StopReason { + STOP_NONE, STOP_FORCE, STOP_RESTART, STOP_EXIT +}; +static StopReason gln_stop_reason = STOP_NONE; + +/* Level 9 standard input prompt string. */ +static const char *const GLN_INPUT_PROMPT = "> "; + +/* + * Typedef equivalents for interpreter types (uncapitalized to avoid appearing + * as macros), and some internal interpreter symbols symbols used for our own + * deviant purposes. + */ +typedef L9BOOL gln_bool; +typedef L9BYTE gln_byte; +typedef L9UINT16 gln_uint16; +typedef L9UINT32 gln_uint32; + +extern void save(void); +extern void restore(void); +extern gln_bool Cheating; +extern gln_byte *startdata; +extern gln_uint32 FileSize; + +/* Forward declarations of event wait and other miscellaneous functions. */ +static void gln_event_wait(glui32 wait_type, event_t *event); +static void gln_event_wait_2(glui32 wait_type_1, + glui32 wait_type_2, event_t *event); + +static void gln_watchdog_tick(void); +static void gln_standout_string(const char *message); + +static int gln_confirm(const char *prompt); + + +/*---------------------------------------------------------------------*/ +/* Glk port utility functions */ +/*---------------------------------------------------------------------*/ + +/* + * gln_fatal() + * + * Fatal error handler. The function returns, expecting the caller to + * abort() or otherwise handle the error. + */ +static void gln_fatal(const char *string) { + /* + * If the failure happens too early for us to have a window, print + * the message to stderr. + */ + if (!gln_main_window) { + warning("INTERNAL ERROR: %s", string); + return; + } + + /* Cancel all possible pending window input events. */ + g_vm->glk_cancel_line_event(gln_main_window, NULL); + g_vm->glk_cancel_char_event(gln_main_window); + + /* Print a message indicating the error. */ + g_vm->glk_set_window(gln_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"); +} + + +/* + * gln_malloc() + * gln_realloc() + * + * Non-failing malloc and realloc; call gln_fatal and exit if memory + * allocation fails. + */ +static void * +gln_malloc(size_t size) { + void *pointer; + + pointer = malloc(size); + if (!pointer) { + gln_fatal("GLK: Out of system memory"); + g_vm->glk_exit(); + } + + return pointer; +} + +static void * +gln_realloc(void *ptr, size_t size) { + void *pointer; + + pointer = realloc(ptr, size); + if (!pointer) { + gln_fatal("GLK: Out of system memory"); + g_vm->glk_exit(); + } + + return pointer; +} + + +/* + * gln_strncasecmp() + * gln_strcasecmp() + * + * Strncasecmp and strcasecmp are not ANSI functions, so here are local + * definitions to do the same jobs. + * + * They're global here so that the core interpreter can use them; otherwise + * it tries to use the non-ANSI str[n]icmp() functions. + */ +int +gln_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; +} + +int +gln_strcasecmp(const char *s1, const char *s2) { + size_t s1len, s2len; + int result; + + s1len = strlen(s1); + s2len = strlen(s2); + + result = gln_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 stub graphics functions */ +/*---------------------------------------------------------------------*/ + +/* + * If we're working with a very stripped down, old, or lazy Glk library + * that neither offers Glk graphics nor graphics stubs functions, here + * we define our own stubs, to avoid link-time errors. + */ +#ifndef GLK_MODULE_IMAGE +static glui32 +g_vm->glk_image_draw(winid_t win, glui32 image, glsi32 val1, glsi32 val2) { + return FALSE; +} +static glui32 +g_vm->glk_image_draw_scaled(winid_t win, glui32 image, glsi32 val1, glsi32 val2, + glui32 width, glui32 height) { + return FALSE; +} +static glui32 +g_vm->glk_image_get_info(glui32 image, glui32 *width, glui32 *height) { + return FALSE; +} +static void +g_vm->glk_window_flow_break(winid_t win) { +} +static void +g_vm->glk_window_erase_rect(winid_t win, glsi32 left, glsi32 top, + glui32 width, glui32 height) { +} +static void +g_vm->glk_window_fill_rect(winid_t win, glui32 color, glsi32 left, glsi32 top, + glui32 width, glui32 height) { +} +static void +g_vm->glk_window_set_background_color(winid_t win, glui32 color) { +} +#endif + + +/*---------------------------------------------------------------------*/ +/* Glk port CRC functions */ +/*---------------------------------------------------------------------*/ + +/* CRC table initialization polynomial. */ +static const gln_uint16 GLN_CRC_POLYNOMIAL = 0xa001; + + +/* + * gln_get_buffer_crc() + * + * Return the CRC of the bytes buffer[0..length-1]. + * + * This algorithm is selected to match the CRCs used in L9cut. Because of + * the odd way CRCs are padded when L9cut calculates the CRC, this function + * allows a count of NUL padding bytes to be included within the return CRC. + */ +static gln_uint16 gln_get_buffer_crc(const void *void_buffer, size_t length, size_t padding) { + static int is_initialized = FALSE; + static gln_uint16 crc_table[BYTE_MAX + 1]; + + const char *buffer = (const char *) void_buffer; + gln_uint16 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 = (gln_uint16) index; + for (bit = 0; bit < BITS_PER_CHAR; bit++) + crc = crc & 1 ? GLN_CRC_POLYNOMIAL ^ (crc >> 1) : crc >> 1; + + crc_table[index] = crc; + } + + is_initialized = TRUE; + + /* CRC lookup table self-test, after is_initialized set -- recursion. */ + assert(gln_get_buffer_crc("123456789", 9, 0) == 0xbb3d); + } + + /* Start with zero in the crc, then update using table entries. */ + crc = 0; + for (index = 0; index < length; index++) + crc = crc_table[(crc ^ buffer[index]) & BYTE_MAX] ^ (crc >> BITS_PER_CHAR); + + /* Add in any requested NUL padding bytes. */ + for (index = 0; index < padding; index++) + crc = crc_table[crc & BYTE_MAX] ^ (crc >> BITS_PER_CHAR); + + return crc; +} + + +/*---------------------------------------------------------------------*/ +/* 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. It's generated on demand, + * and may be re-requested when, say, the game changes, perhaps by moving to + * the next part of a multipart game. + */ +static const char *gln_gameid_game_name = NULL; + + +/* + * The following game database is obtained from L9cut's l9data_d.h, and + * lets us find a game's name from its data CRC. Entries marked "WANTED" in + * l9data_d.h, and file commentary, have been removed for brevity, and the + * file has been reformatted (patchlevel data removed). + * + * The version of l9data_d.h used is 050 (22 Oct 2002). + */ +typedef const struct { + const gln_uint16 length; /* Datafile length in bytes */ + const gln_byte checksum; /* 8-bit checksum, last datafile byte */ + const gln_uint16 crc; /* 16-bit CRC, L9cut-internal */ + const char *const name; /* Game title and platform */ +} gln_game_table_t; +typedef gln_game_table_t *gln_game_tableref_t; + +static gln_game_table_t GLN_GAME_TABLE[] = { + {0x5323, 0xb7, 0x8af7, "Adventure Quest (Amstrad CPC/Spectrum)"}, + + {0x630e, 0x8d, 0x7d7d, "Dungeon Adventure (Amstrad CPC)"}, + {0x630e, 0xbe, 0x3374, "Dungeon Adventure (MSX)"}, + + {0x5eb9, 0x30, 0xe99a, "Lords of Time (Amstrad CPC)"}, + {0x5eb9, 0x5d, 0xc098, "Lords of Time (MSX)"}, + {0x5eb9, 0x6e, 0xc689, "Lords of Time (Spectrum)"}, + + {0x5fab, 0x5c, 0xa309, "Snowball (Amstrad CPC)"}, + {0x5fab, 0x2f, 0x8aa2, "Snowball (MSX)"}, + + {0x60c4, 0x28, 0x0154, "Return to Eden (Amstrad CPC/Commodore 64[v1])"}, + {0x6064, 0x01, 0x5b3c, "Return to Eden (BBC[v1])"}, + {0x6064, 0x95, 0x510c, "Return to Eden (Commodore 64[v2])"}, + {0x6064, 0xda, 0xe610, "Return to Eden (Commodore 64[v2] *corrupt*)"}, + {0x6064, 0xbd, 0x73ec, "Return to Eden (Atari *corrupt*)"}, + {0x6047, 0x6c, 0x17ab, "Return to Eden (BBC[v2])"}, + {0x5ca1, 0x33, 0x1c43, "Return to Eden (Spectrum[v1])"}, + {0x5cb7, 0x64, 0x0790, "Return to Eden (Spectrum[v2])"}, + {0x5cb7, 0xfe, 0x3533, "Return to Eden (MSX)"}, + + {0x34b3, 0x20, 0xccda, "Erik the Viking (BBC/Commodore 64)"}, + {0x34b3, 0x53, 0x8f00, "Erik the Viking (Spectrum)"}, + {0x34b3, 0xc7, 0x9058, "Erik the Viking (Amstrad CPC)"}, + + { + 0x63be, 0xd6, 0xcf5d, + "Emerald Isle (Atari/Commodore 64/Amstrad CPC/Spectrum)" + }, + {0x63be, 0x0a, 0x21ed, "Emerald Isle (MSX *corrupt*)"}, + {0x378c, 0x8d, 0x3a21, "Emerald Isle (BBC)"}, + + {0x506c, 0xf0, 0xba72, "Red Moon (BBC/Commodore 64/Amstrad CPC/MSX)"}, + {0x505d, 0x32, 0x2dcf, "Red Moon (Spectrum)"}, + + {0x772b, 0xcd, 0xa503, "Worm in Paradise (Spectrum 128)"}, + {0x546c, 0xb7, 0x9420, "Worm in Paradise (Spectrum 48)"}, + {0x6d84, 0xf9, 0x49ae, "Worm in Paradise (Commodore 64 *corrupt*)"}, + {0x6d84, 0xc8, 0x943f, "Worm in Paradise (Commodore 64 *fixed*)"}, + {0x6030, 0x47, 0x46ad, "Worm in Paradise (Amstrad CPC)"}, + {0x5828, 0xbd, 0xe7cb, "Worm in Paradise (BBC)"}, + + {0x7410, 0x5e, 0x60be, "Price of Magik (Spectrum 128)"}, + {0x5aa4, 0xc1, 0x10a0, "Price of Magik (Spectrum 48[v1])"}, + {0x5aa4, 0xc1, 0xeda4, "Price of Magik (Spectrum 48[v2])"}, + {0x6fc6, 0x14, 0xf9b6, "Price of Magik (Commodore 64)"}, + {0x5aa4, 0xc1, 0xbbf4, "Price of Magik (Amstrad CPC)"}, + {0x5671, 0xbc, 0xff35, "Price of Magik (BBC)"}, + + {0x76f4, 0x5e, 0x1fe5, "Colossal Adventure /JoD (Amiga/PC)"}, + {0x76f4, 0x5a, 0xcf4b, "Colossal Adventure /JoD (ST)"}, + {0x6e60, 0x83, 0x18e0, "Adventure Quest /JoD (Amiga/PC)"}, + {0x6e5c, 0xf6, 0xd356, "Adventure Quest /JoD (ST)"}, + {0x6f0c, 0x95, 0x1f64, "Dungeon Adventure /JoD (Amiga/PC/ST)"}, + + {0x6f70, 0x40, 0xbd91, "Colossal Adventure /JoD (MSX)"}, + + {0x6f6e, 0x78, 0x28cd, "Colossal Adventure /JoD (Spectrum 128)"}, + {0x6970, 0xd6, 0xa820, "Adventure Quest /JoD (Spectrum 128)"}, + {0x6de8, 0x4c, 0xd795, "Dungeon Adventure /JoD (Spectrum 128)"}, + + { + 0x6f4d, 0xcb, 0xe8f2, + "Colossal Adventure /JoD (Amstrad CPC128[v1]/Spectrum +3)" + }, + {0x6f6a, 0xa5, 0x8dd2, "Colossal Adventure /JoD (Amstrad CPC128[v2])"}, + {0x6968, 0x32, 0x0c01, "Adventure Quest /JoD (Amstrad CPC128/Spectrum +3)"}, + {0x6dc0, 0x63, 0x5d95, "Dungeon Adventure /JoD (Amstrad CPC128/Spectrum +3)"}, + + {0x5e31, 0x7c, 0xaa54, "Colossal Adventure /JoD (Amstrad CPC64)"}, + {0x5b50, 0x66, 0x1800, "Adventure Quest /JoD (Amstrad CPC64)"}, + {0x58a6, 0x24, 0xb50f, "Dungeon Adventure /JoD (Amstrad CPC64)"}, + + {0x6c8e, 0xb6, 0x9be3, "Colossal Adventure /JoD (Commodore 64)"}, + {0x63b6, 0x2e, 0xef38, "Adventure Quest /JoD (Commodore 64)"}, + {0x6bd2, 0x65, 0xa41f, "Dungeon Adventure /JoD (Commodore 64)"}, + + {0x5b16, 0x3b, 0xe2aa, "Colossal Adventure /JoD (Atari)"}, + {0x5b58, 0x50, 0x332e, "Adventure Quest /JoD (Atari)"}, + {0x593a, 0x80, 0x7a34, "Dungeon Adventure /JoD (Atari)"}, + + {0x5a8e, 0xf2, 0x7cca, "Colossal Adventure /JoD (Spectrum 48)"}, + {0x5ace, 0x11, 0xdc12, "Adventure Quest /JoD (Spectrum 48)"}, + {0x58a3, 0x38, 0x8ce4, "Dungeon Adventure /JoD (Spectrum 48)"}, + + {0x7b31, 0x6e, 0x2e2b, "Snowball /SD (Amiga/ST)"}, + {0x7d16, 0xe6, 0x5438, "Return to Eden /SD (Amiga/ST)"}, + {0x7cd9, 0x0c, 0x4df1, "Worm in Paradise /SD (Amiga/ST)"}, + + {0x7b2f, 0x70, 0x6955, "Snowball /SD (Mac/PC/Spectrum 128)"}, + {0x7b2f, 0x70, 0x6f6c, "Snowball /SD (Amstrad CPC/Spectrum +3)"}, + {0x7d14, 0xe8, 0xfbab, "Return to Eden /SD (PC)"}, + {0x7cff, 0xf8, 0x6044, "Return to Eden /SD (Amstrad CPC/Spectrum +3)"}, + {0x7cf8, 0x24, 0x9c1c, "Return to Eden /SD (Mac)"}, + {0x7c55, 0x18, 0xdaee, "Return to Eden /SD (Spectrum 128)"}, + { + 0x7cd7, 0x0e, 0x4feb, + "Worm in Paradise /SD (Amstrad CPC/Mac/PC/Spectrum 128/Spectrum +3)" + }, + + {0x7363, 0x65, 0xa0ab, "Snowball /SD (Commodore 64)"}, + {0x772f, 0xca, 0x8602, "Return to Eden /SD (Commodore 64)"}, + {0x788d, 0x72, 0x888a, "Worm in Paradise /SD (Commodore 64)"}, + + {0x6bf8, 0x3f, 0xc9f7, "Snowball /SD (Atari)"}, + {0x60f7, 0x68, 0xc2bc, "Return to Eden /SD (Atari)"}, + {0x6161, 0xf3, 0xe6d7, "Worm in Paradise /SD (Atari)"}, + + {0x67a3, 0x9d, 0x1d05, "Snowball /SD (Apple ][)"}, + {0x639c, 0x8b, 0x06e2, "Return to Eden /SD (Apple ][)"}, + {0x60dd, 0xf2, 0x5bb8, "Worm in Paradise /SD (Apple ][)"}, + + {0x6541, 0x02, 0x2e6c, "Snowball /SD (Spectrum 48)"}, + {0x5f43, 0xca, 0x828c, "Return to Eden /SD (Spectrum 48)"}, + {0x5ebb, 0xf1, 0x4dec, "Worm in Paradise /SD (Spectrum 48)"}, + + {0x8333, 0xb7, 0xe2ac, "Adrian Mole I, pt. 1 (Commodore 64)"}, + {0x844d, 0x50, 0x5353, "Adrian Mole I, pt. 2 (Commodore 64)"}, + {0x8251, 0x5f, 0x862a, "Adrian Mole I, pt. 3 (Commodore 64)"}, + {0x7a78, 0x5e, 0x6ea3, "Adrian Mole I, pt. 4 (Commodore 64)"}, + + {0x7c6f, 0x0f, 0xba24, "Adrian Mole I, pt. 1 (Amstrad CPC)"}, + + {0x72fa, 0x8b, 0x6f12, "Adrian Mole I, pt. 1 (Spectrum)"}, + {0x738e, 0x5b, 0x7e3d, "Adrian Mole I, pt. 2 (Spectrum)"}, + {0x7375, 0xe5, 0x3f3e, "Adrian Mole I, pt. 3 (Spectrum)"}, + {0x78d5, 0xe3, 0xcd7d, "Adrian Mole I, pt. 4 (Spectrum)"}, + + {0x3a31, 0xe5, 0x0bdb, "Adrian Mole I, pt. 1 (BBC)"}, + {0x37f1, 0x77, 0xd231, "Adrian Mole I, pt. 2 (BBC)"}, + {0x3900, 0x1c, 0x5d9a, "Adrian Mole I, pt. 3 (BBC)"}, + {0x3910, 0xac, 0x07f9, "Adrian Mole I, pt. 4 (BBC)"}, + {0x3ad6, 0xa7, 0x95d2, "Adrian Mole I, pt. 5 (BBC)"}, + {0x38a5, 0x0f, 0xdefc, "Adrian Mole I, pt. 6 (BBC)"}, + {0x361e, 0x7e, 0xfd9f, "Adrian Mole I, pt. 7 (BBC)"}, + {0x3934, 0x75, 0xe141, "Adrian Mole I, pt. 8 (BBC)"}, + {0x3511, 0xcc, 0xd829, "Adrian Mole I, pt. 9 (BBC)"}, + {0x38dd, 0x31, 0x2534, "Adrian Mole I, pt. 10 (BBC)"}, + {0x39c0, 0x44, 0x89df, "Adrian Mole I, pt. 11 (BBC)"}, + {0x3a12, 0x8f, 0xc2bd, "Adrian Mole I, pt. 12 (BBC)"}, + + {0x7931, 0xb9, 0xc51b, "Adrian Mole II, pt. 1 (Commodore 64/Amstrad CPC)"}, + {0x7cdf, 0xa5, 0x43e3, "Adrian Mole II, pt. 2 (Commodore 64/Amstrad CPC)"}, + {0x7a0c, 0x97, 0x4bea, "Adrian Mole II, pt. 3 (Commodore 64/Amstrad CPC)"}, + {0x7883, 0xe2, 0xee0e, "Adrian Mole II, pt. 4 (Commodore 64/Amstrad CPC)"}, + + {0x6841, 0x4a, 0x94e7, "Adrian Mole II, pt. 1 (Spectrum)"}, + {0x6bc0, 0x62, 0xab3d, "Adrian Mole II, pt. 2 (Spectrum)"}, + {0x692c, 0x21, 0x2015, "Adrian Mole II, pt. 3 (Spectrum)"}, + {0x670a, 0x94, 0xa2a6, "Adrian Mole II, pt. 4 (Spectrum)"}, + + {0x593a, 0xaf, 0x30e9, "Adrian Mole II, pt. 1 (BBC)"}, + {0x57e6, 0x8a, 0xc41a, "Adrian Mole II, pt. 2 (BBC)"}, + {0x5819, 0xcd, 0x1ba0, "Adrian Mole II, pt. 3 (BBC)"}, + {0x579b, 0xad, 0xa723, "Adrian Mole II, pt. 4 (BBC)"}, + + {0x765d, 0xcd, 0xfc02, "The Archers, pt. 1 (Commodore 64)"}, + {0x6e58, 0x07, 0xbffc, "The Archers, pt. 2 (Commodore 64)"}, + {0x7e98, 0x6a, 0x95e5, "The Archers, pt. 3 (Commodore 64)"}, + {0x81e2, 0xd5, 0xb278, "The Archers, pt. 4 (Commodore 64)"}, + + {0x6ce5, 0x58, 0x46de, "The Archers, pt. 1 (Spectrum)"}, + {0x68da, 0xc1, 0x3b8e, "The Archers, pt. 2 (Spectrum)"}, + {0x6c67, 0x9a, 0x9a6a, "The Archers, pt. 3 (Spectrum)"}, + {0x6d91, 0xb9, 0x12a7, "The Archers, pt. 4 (Spectrum)"}, + + {0x5834, 0x42, 0xcc9d, "The Archers, pt. 1 (BBC)"}, + {0x56dd, 0x51, 0xe582, "The Archers, pt. 2 (BBC)"}, + {0x5801, 0x53, 0xf2ef, "The Archers, pt. 3 (BBC)"}, + {0x54a4, 0x01, 0xc0ab, "The Archers, pt. 4 (BBC)"}, + + {0x579e, 0x97, 0x9faa, "Lords of Time /T&M GD (BBC)"}, + {0x5500, 0x50, 0xca75, "Red Moon /T&M GD (BBC)"}, + {0x579a, 0x2a, 0x9373, "Price of Magik /T&M GD (BBC)"}, + + {0x4fd2, 0x9d, 0x799a, "Lancelot, pt. 1 GD (BBC)"}, + {0x4dac, 0xa8, 0x86ed, "Lancelot, pt. 2 GD (BBC)"}, + {0x4f96, 0x22, 0x30f8, "Lancelot, pt. 3 GD (BBC)"}, + + {0x55ce, 0xa1, 0xba12, "Scapeghost, pt. 1 GD (BBC)"}, + {0x54a6, 0xa9, 0xc9f3, "Scapeghost, pt. 2 GD (BBC)"}, + {0x51bc, 0xe3, 0x89c3, "Scapeghost, pt. 3 GD (BBC)"}, + + {0x46ec, 0x64, 0x2300, "Knight Orc, pt. 1 GD (Amstrad CPC/Spectrum +3)"}, + {0x6140, 0x18, 0x4f66, "Knight Orc, pt. 2 GD (Amstrad CPC/Spectrum +3)"}, + {0x640e, 0xc1, 0xfc69, "Knight Orc, pt. 3 GD (Amstrad CPC/Spectrum +3)"}, + + {0x5ff0, 0xf8, 0x3a13, "Gnome Ranger, pt. 1 GD (Amstrad CPC/Spectrum +3)"}, + {0x6024, 0x01, 0xaaa9, "Gnome Ranger, pt. 2 GD (Amstrad CPC/Spectrum +3)"}, + {0x6036, 0x3d, 0x6c6c, "Gnome Ranger, pt. 3 GD (Amstrad CPC/Spectrum +3)"}, + + {0x69fe, 0x56, 0xecfb, "Lords of Time /T&M GD (Amstrad CPC/Spectrum +3)"}, + {0x6888, 0x8d, 0x7f6a, "Red Moon /T&M GD (Amstrad CPC/Spectrum +3)"}, + {0x5a50, 0xa9, 0xa5fa, "Price of Magik /T&M GD (Amstrad CPC/Spectrum +3)"}, + + {0x5c7a, 0x44, 0x460e, "Lancelot, pt. 1 GD (Amstrad CPC/Spectrum +3)"}, + {0x53a2, 0x1e, 0x2fae, "Lancelot, pt. 2 GD (Amstrad CPC/Spectrum +3)"}, + {0x5914, 0x22, 0x4a31, "Lancelot, pt. 3 GD (Amstrad CPC/Spectrum +3)"}, + + {0x5a38, 0xf7, 0x876e, "Ingrid's Back, pt. 1 GD (Amstrad CPC/Spectrum +3)"}, + {0x531a, 0xed, 0xcf3f, "Ingrid's Back, pt. 2 GD (Amstrad CPC/Spectrum +3)"}, + {0x57e4, 0x19, 0xb354, "Ingrid's Back, pt. 3 GD (Amstrad CPC/Spectrum +3)"}, + + {0x5cbc, 0xa5, 0x0dbe, "Scapeghost, pt. 1 GD (Amstrad CPC/Spectrum +3)"}, + {0x5932, 0x4e, 0xb2f5, "Scapeghost, pt. 2 GD (Amstrad CPC/Spectrum +3)"}, + {0x5860, 0x95, 0x3227, "Scapeghost, pt. 3 GD (Amstrad CPC/Spectrum +3)"}, + + {0x74e0, 0x92, 0x885e, "Knight Orc, pt. 1 GD (Spectrum 128)"}, + {0x6dbc, 0x97, 0x6f55, "Knight Orc, pt. 2 GD (Spectrum 128)"}, + {0x7402, 0x07, 0x385f, "Knight Orc, pt. 3 GD (Spectrum 128)"}, + + {0x52aa, 0xdf, 0x7b5b, "Gnome Ranger, pt. 1 GD (Spectrum 128)"}, + {0x6ffa, 0xdb, 0xdde2, "Gnome Ranger, pt. 2 GD (Spectrum 128)"}, + {0x723a, 0x69, 0x039b, "Gnome Ranger, pt. 3 GD (Spectrum 128)"}, + + {0x6f1e, 0xda, 0x2ce0, "Lords of Time /T&M GD (Spectrum 128)"}, + {0x6da0, 0xb8, 0x3802, "Red Moon /T&M GD (Spectrum 128)"}, + {0x6108, 0xdd, 0xefe7, "Price of Magik /T&M GD (Spectrum 128)"}, + + {0x768c, 0xe8, 0x8fc6, "Lancelot, pt. 1 GD (Spectrum 128)"}, + {0x76b0, 0x1d, 0x0fcd, "Lancelot, pt. 2 GD (Spectrum 128)"}, + {0x765e, 0x4f, 0x3b73, "Lancelot, pt. 3 GD (Spectrum 128)"}, + + {0x76a0, 0x3a, 0xb803, "Ingrid's Back, pt. 1 GD (Spectrum 128)"}, + {0x7674, 0x0b, 0xe92f, "Ingrid's Back, pt. 2 GD (Spectrum 128)"}, + {0x765e, 0xba, 0x086d, "Ingrid's Back, pt. 3 GD (Spectrum 128)"}, + + {0x762e, 0x82, 0x8848, "Scapeghost, pt. 1 GD (Spectrum 128)"}, + {0x5bd6, 0x35, 0x79ef, "Scapeghost, pt. 2 GD (Spectrum 128)"}, + {0x6fa8, 0xa4, 0x62c2, "Scapeghost, pt. 3 GD (Spectrum 128)"}, + + {0xbb93, 0x36, 0x6a05, "Knight Orc, pt. 1 (Amiga)"}, + {0xbb6e, 0xad, 0x4d40, "Knight Orc, pt. 1 (ST)"}, + {0xc58e, 0x4a, 0x4e9d, "Knight Orc, pt. 2 (Amiga/ST)"}, + {0xcb9a, 0x0f, 0x0804, "Knight Orc, pt. 3 (Amiga/ST)"}, + + {0xbb6e, 0xa6, 0x9753, "Knight Orc, pt. 1 (PC)"}, + {0xc58e, 0x43, 0xe9ce, "Knight Orc, pt. 2 (PC)"}, + {0xcb9a, 0x08, 0x6c36, "Knight Orc, pt. 3 (PC)"}, + + {0x898a, 0x43, 0xfc8b, "Knight Orc, pt. 1 (Apple ][)"}, + {0x8b9f, 0x61, 0x7288, "Knight Orc, pt. 2 (Apple ][)"}, + {0x8af9, 0x61, 0x7542, "Knight Orc, pt. 3 (Apple ][)"}, + + {0x8970, 0x6b, 0x3c7b, "Knight Orc, pt. 1 (Commodore 64 Gfx)"}, + {0x8b90, 0x4e, 0x098c, "Knight Orc, pt. 2 (Commodore 64 Gfx)"}, + {0x8aea, 0x4e, 0xca54, "Knight Orc, pt. 3 (Commodore 64 Gfx)"}, + + {0x86d0, 0xb7, 0xadbd, "Knight Orc, pt. 1 (Spectrum 48)"}, + {0x8885, 0x22, 0xe293, "Knight Orc, pt. 2 (Spectrum 48)"}, + {0x87e5, 0x0e, 0xdc33, "Knight Orc, pt. 3 (Spectrum 48)"}, + + {0xb1a9, 0x80, 0x5fb7, "Gnome Ranger, pt. 1 (Amiga/ST)"}, + {0xab9d, 0x31, 0xbe6d, "Gnome Ranger, pt. 2 (Amiga/ST)"}, + {0xae28, 0x87, 0xb6b6, "Gnome Ranger, pt. 3 (Amiga/ST)"}, + + {0xb0ec, 0xc2, 0x0053, "Gnome Ranger, pt. 1 (ST[v1])"}, + {0xaf82, 0x83, 0x19f7, "Gnome Ranger, pt. 2 (ST[v1])"}, + + {0xb1aa, 0xad, 0xaf47, "Gnome Ranger, pt. 1 (PC)"}, + {0xb19e, 0x92, 0x8f96, "Gnome Ranger, pt. 1 (ST[v2])"}, + {0xab8b, 0xbf, 0x31e6, "Gnome Ranger, pt. 2 (PC/ST[v2])"}, + {0xae16, 0x81, 0x8741, "Gnome Ranger, pt. 3 (PC/ST[v2])"}, + + {0xad41, 0xa8, 0x42c5, "Gnome Ranger, pt. 1 (Commodore 64 TO)"}, + {0xa735, 0xf7, 0x2e08, "Gnome Ranger, pt. 2 (Commodore 64 TO)"}, + {0xa9c0, 0x9e, 0x0d70, "Gnome Ranger, pt. 3 (Commodore 64 TO)"}, + + {0x908e, 0x0d, 0x58a7, "Gnome Ranger, pt. 1 (Commodore 64 Gfx)"}, + {0x8f6f, 0x0a, 0x411a, "Gnome Ranger, pt. 2 (Commodore 64 Gfx)"}, + {0x9060, 0xbb, 0xe75d, "Gnome Ranger, pt. 3 (Commodore 64 Gfx)"}, + + {0x8aab, 0xc0, 0xde5f, "Gnome Ranger, pt. 1 (Spectrum 48)"}, + {0x8ac8, 0x9a, 0xc89b, "Gnome Ranger, pt. 2 (Spectrum 48)"}, + {0x8a93, 0x4f, 0x10cc, "Gnome Ranger, pt. 3 (Spectrum 48)"}, + + {0xb57c, 0x44, 0x7779, "Lords of Time /T&M (PC)"}, + {0xa69e, 0x6c, 0xb268, "Red Moon /T&M (PC)"}, + {0xbac7, 0x7f, 0xddb2, "Price of Magik /T&M (PC)"}, + + {0xb579, 0x89, 0x3e89, "Lords of Time /T&M (ST)"}, + {0xa698, 0x41, 0xcaca, "Red Moon /T&M (ST)"}, + {0xbac4, 0x80, 0xa750, "Price of Magik /T&M (ST)"}, + + {0xb576, 0x2a, 0x7239, "Lords of Time /T&M (Amiga)"}, + {0xa692, 0xd1, 0x6a99, "Red Moon /T&M (Amiga)"}, + {0xbaca, 0x3a, 0x221b, "Price of Magik /T&M (Amiga)"}, + + {0xb563, 0x6a, 0x0c5c, "Lords of Time /T&M (Mac)"}, + {0xa67c, 0xb8, 0xff41, "Red Moon /T&M (Mac)"}, + {0xbab2, 0x87, 0x09f5, "Price of Magik /T&M (Mac)"}, + + {0xb38c, 0x37, 0x9f8e, "Lords of Time /T&M (Commodore 64 TO)"}, + {0xa4e2, 0xa6, 0x016d, "Red Moon /T&M (Commodore 64 TO)"}, + {0xb451, 0xa8, 0x2682, "Price of Magik /T&M (Commodore 64 TO)"}, + + {0x9070, 0x43, 0x45d4, "Lords of Time /T&M (Commodore 64 Gfx)"}, + {0x903f, 0x6b, 0x603e, "Red Moon /T&M (Commodore 64 Gfx)"}, + {0x8f51, 0xb2, 0x6c9a, "Price of Magik /T&M (Commodore 64 Gfx)"}, + + {0x8950, 0xa1, 0xbb16, "Lords of Time /T&M (Spectrum 48)"}, + {0x8813, 0x11, 0x22de, "Red Moon /T&M (Spectrum 48)"}, + {0x8a60, 0x2a, 0x29ed, "Price of Magik /T&M (Spectrum 48)"}, + + {0xb260, 0xe5, 0xc5b2, "Lords of Time /T&M (PC/ST *USA*)"}, + {0xa3a4, 0xdf, 0x6732, "Red Moon /T&M (PC/ST *USA*)"}, + {0xb7a0, 0x7e, 0x2226, "Price of Magik /T&M (PC/ST *USA*)"}, + + {0xb257, 0xf8, 0xfbd5, "Lords of Time /T&M (Amiga *USA*)"}, + {0xa398, 0x82, 0xd031, "Red Moon /T&M (Amiga *USA*)"}, + {0xb797, 0x1f, 0x84a9, "Price of Magik /T&M (Amiga *USA*)"}, + + {0x8d78, 0x3a, 0xba6e, "Lords of Time /T&M (Commodore 64 Gfx *USA*)"}, + {0x8d56, 0xd3, 0x146a, "Red Moon /T&M (Commodore 64 Gfx *USA*)"}, + {0x8c46, 0xf0, 0xcaf6, "Price of Magik /T&M (Commodore 64 Gfx *USA*)"}, + + {0xc0cf, 0x4e, 0xb7fa, "Lancelot, pt. 1 (Amiga/PC/ST)"}, + {0xd5e9, 0x6a, 0x4192, "Lancelot, pt. 2 (Amiga/PC/ST)"}, + {0xbb8f, 0x1a, 0x7487, "Lancelot, pt. 3 (Amiga/PC/ST)"}, + + {0xc0bd, 0x57, 0x6ef1, "Lancelot, pt. 1 (Mac)"}, + {0xd5d7, 0x99, 0x770b, "Lancelot, pt. 2 (Mac)"}, + {0xbb7d, 0x17, 0xbc42, "Lancelot, pt. 3 (Mac)"}, + + {0xb4c9, 0x94, 0xd784, "Lancelot, pt. 1 (Commodore 64 TO)"}, + {0xb729, 0x51, 0x8ee5, "Lancelot, pt. 2 (Commodore 64 TO)"}, + {0xb702, 0xe4, 0x1809, "Lancelot, pt. 3 (Commodore 64 TO)"}, + + {0x8feb, 0xba, 0xa800, "Lancelot, pt. 1 (Commodore 64 Gfx)"}, + {0x8f6b, 0xfa, 0x0f7e, "Lancelot, pt. 2 (Commodore 64 Gfx)"}, + {0x8f71, 0x2f, 0x0ddc, "Lancelot, pt. 3 (Commodore 64 Gfx)"}, + + {0x8ade, 0xf2, 0xfffb, "Lancelot, pt. 1 (Spectrum 48)"}, + {0x8b0e, 0xfb, 0x0bab, "Lancelot, pt. 2 (Spectrum 48)"}, + {0x8ab3, 0xc1, 0xcb62, "Lancelot, pt. 3 (Spectrum 48)"}, + + {0xbba4, 0x94, 0x0871, "Lancelot, pt. 1 (Amiga/PC *USA*)"}, + {0xd0c0, 0x56, 0x8c48, "Lancelot, pt. 2 (Amiga/PC *USA*)"}, + {0xb6ac, 0xc6, 0xaea0, "Lancelot, pt. 3 (Amiga/PC *USA*)"}, + + {0x8afc, 0x07, 0x8321, "Lancelot, pt. 1 (Commodore 64 Gfx *USA*)"}, + {0x8aec, 0x13, 0x6791, "Lancelot, pt. 2 (Commodore 64 Gfx *USA*)"}, + {0x8aba, 0x0d, 0x5602, "Lancelot, pt. 3 (Commodore 64 Gfx *USA*)"}, + + {0xd19b, 0xad, 0x306d, "Ingrid's Back, pt. 1 (PC)"}, + {0xc5a5, 0xfe, 0x3c98, "Ingrid's Back, pt. 2 (PC)"}, + {0xd7ae, 0x9e, 0x1878, "Ingrid's Back, pt. 3 (PC)"}, + + {0xd188, 0x13, 0xdc60, "Ingrid's Back, pt. 1 (Amiga)"}, + {0xc594, 0x03, 0xea95, "Ingrid's Back, pt. 2 (Amiga)"}, + {0xd79f, 0xb5, 0x1661, "Ingrid's Back, pt. 3 (Amiga)"}, + + {0xd183, 0x83, 0xef72, "Ingrid's Back, pt. 1 (ST)"}, + {0xc58f, 0x65, 0xf337, "Ingrid's Back, pt. 2 (ST)"}, + {0xd79a, 0x57, 0x49c5, "Ingrid's Back, pt. 3 (ST)"}, + + {0xb770, 0x03, 0x9a03, "Ingrid's Back, pt. 1 (Commodore 64 TO)"}, + {0xb741, 0xb6, 0x2aa5, "Ingrid's Back, pt. 2 (Commodore 64 TO)"}, + {0xb791, 0xa1, 0xd065, "Ingrid's Back, pt. 3 (Commodore 64 TO)"}, + + {0x9089, 0xce, 0xc5e2, "Ingrid's Back, pt. 1 (Commodore 64 Gfx)"}, + {0x908d, 0x80, 0x30c7, "Ingrid's Back, pt. 2 (Commodore 64 Gfx)"}, + {0x909e, 0x9f, 0xdecc, "Ingrid's Back, pt. 3 (Commodore 64 Gfx)"}, + + {0x8ab7, 0x68, 0xee57, "Ingrid's Back, pt. 1 (Spectrum 48)"}, + {0x8b1e, 0x84, 0x2538, "Ingrid's Back, pt. 2 (Spectrum 48)"}, + {0x8b1c, 0xa8, 0x9262, "Ingrid's Back, pt. 3 (Spectrum 48)"}, + + {0xbeab, 0x2d, 0x94d9, "Scapeghost, pt. 1 (Amiga)"}, + {0xc132, 0x14, 0x7adc, "Scapeghost, pt. 1 (Amiga *bak*)"}, + {0xbe94, 0xcc, 0x04b8, "Scapeghost, pt. 1 (PC/ST)"}, + {0x99bd, 0x65, 0x032e, "Scapeghost, pt. 2 (Amiga/PC/ST)"}, + {0xbcb6, 0x7a, 0x7d4f, "Scapeghost, pt. 3 (Amiga/PC/ST)"}, + + {0x9058, 0xcf, 0x9748, "Scapeghost, pt. 1 (Commodore 64 Gfx)"}, + {0x8f43, 0xc9, 0xeefd, "Scapeghost, pt. 2 (Commodore 64 Gfx)"}, + {0x90ac, 0x68, 0xb4a8, "Scapeghost, pt. 3 (Commodore 64 Gfx)"}, + + {0x8a21, 0xf4, 0xd9e4, "Scapeghost, pt. 1 (Spectrum 48)"}, + {0x8a12, 0xe3, 0xc2ff, "Scapeghost, pt. 2 (Spectrum 48)"}, + {0x8a16, 0xcc, 0x4f3b, "Scapeghost, pt. 3 (Spectrum 48)"}, + + {0x3ebb, 0x00, 0xf6dc, "Champion of the Raj (English) 1/2 GD (Amiga)"}, + {0x0fd8, 0x00, 0xf250, "Champion of the Raj (English) 2/2 GD (Amiga)"}, + + {0x3e8f, 0x00, 0x5599, "Champion of the Raj (English) 1/2 GD (ST)"}, + + {0x3e4f, 0x00, 0xb202, "Champion of the Raj (English) 1/2 GD (PC)"}, + {0x14a3, 0x00, 0xa288, "Champion of the Raj (English) 2/2 GD (PC)"}, + + {0x1929, 0x00, 0xd4b2, "Champion of the Raj (demo), 1/2 GD (ST)"}, + {0x40e0, 0x02, 0x080d, "Champion of the Raj (demo), 2/2 GD (ST)"}, + + {0x4872, 0x00, 0x9515, "Champion of the Raj (German) 1/2 GD (Amiga)"}, + {0x11f5, 0x00, 0xbf39, "Champion of the Raj (German) 2/2 GD (Amiga)"}, + + {0x4846, 0x00, 0xd9c1, "Champion of the Raj (German) 1/2 GD (ST)"}, + {0x11f5, 0x00, 0x7aa4, "Champion of the Raj (German) 2/2 GD (ST)"}, + + {0x110f, 0x00, 0x4b57, "Champion of the Raj (French) 2/2 GD (ST)"}, + + {0x0000, 0x00, 0x0000, NULL} +}; + + +/* + * The following patch database is obtained from L9cut's l9data_p.h, and + * allows CRCs from patched games to be translated into original CRCs for + * lookup in the game database above. Some file commentary has been removed + * for brevity, and unused patch fields deleted. + * + * The version of l9data_p.h used is 012 (22 May 2001). + */ +typedef const struct { + const gln_uint16 length; /* Datafile length in bytes */ + const gln_byte orig_checksum; /* 8-bit checksum, last datafile byte */ + const gln_uint16 orig_crc; /* 16-bit CRC, L9cut-internal */ + const gln_byte patch_checksum; /* 8-bit checksum, last datafile byte */ + const gln_uint16 patch_crc; /* 16-bit CRC, L9cut-internal */ +} gln_patch_table_t; +typedef gln_patch_table_t *gln_patch_tableref_t; + +static gln_patch_table_t GLN_PATCH_TABLE[] = { + /* Price of Magik (Spectrum128) */ + {0x7410, 0x5e, 0x60be, 0x70, 0x6cef}, + + /* Price of Magik (Commodore 64) */ + {0x6fc6, 0x14, 0xf9b6, 0x26, 0x3326}, + + /* Price of Magik (Spectrum48) */ + {0x5aa4, 0xc1, 0xeda4, 0xd3, 0xed35}, + {0x5aa4, 0xc1, 0xeda4, 0xc1, 0x8a65}, + + /* Colossal Adventure /JoD (Amiga/PC) */ + {0x76f4, 0x5e, 0x1fe5, 0xea, 0x1305}, + {0x76f4, 0x5e, 0x1fe5, 0xb5, 0x901f}, + {0x76f4, 0x5e, 0x1fe5, 0x5e, 0x6ea1}, + + /* Colossal Adventure /JoD (ST) */ + {0x76f4, 0x5a, 0xcf4b, 0xe6, 0x012a}, + {0x76f4, 0x5a, 0xcf4b, 0xb1, 0x40b1}, + + /* Adventure Quest /JoD (Amiga/PC) */ + {0x6e60, 0x83, 0x18e0, 0x4c, 0xcfb0}, + {0x6e60, 0x83, 0x18e0, 0xfa, 0x9b3b}, + {0x6e60, 0x83, 0x18e0, 0x83, 0x303d}, + + /* Adventure Quest /JoD (ST) */ + {0x6e5c, 0xf6, 0xd356, 0xbf, 0xede7}, + {0x6e5c, 0xf6, 0xd356, 0x6d, 0x662d}, + + /* Dungeon Adventure /JoD (Amiga/PC/ST) */ + {0x6f0c, 0x95, 0x1f64, 0x6d, 0x2443}, + {0x6f0c, 0x95, 0x1f64, 0x0c, 0x6066}, + {0x6f0c, 0x95, 0x1f64, 0x96, 0xdaca}, + {0x6f0c, 0x95, 0x1f64, 0x95, 0x848d}, + + /* Colossal Adventure /JoD (Spectrum128) */ + {0x6f6e, 0x78, 0x28cd, 0xf8, 0xda5f}, + {0x6f6e, 0x78, 0x28cd, 0x77, 0x5b4e}, + + /* Adventure Quest /JoD (Spectrum128) */ + {0x6970, 0xd6, 0xa820, 0x3b, 0x1870}, + {0x6970, 0xd6, 0xa820, 0xd5, 0x13c4}, + + /* Dungeon Adventure /JoD (Spectrum128) */ + {0x6de8, 0x4c, 0xd795, 0xa2, 0x3eea}, + {0x6de8, 0x4c, 0xd795, 0x4b, 0xad30}, + + /* Colossal Adventure /JoD (Amstrad CPC) */ + {0x6f4d, 0xcb, 0xe8f2, 0x4b, 0xb384}, + {0x6f4d, 0xcb, 0xe8f2, 0xca, 0x96e7}, + + /* Adventure Quest /JoD (Amstrad CPC) */ + {0x6968, 0x32, 0x0c01, 0x97, 0xdded}, + {0x6968, 0x32, 0x0c01, 0x31, 0xe8c2}, + + /* Dungeon Adventure /JoD (Amstrad CPC) */ + {0x6dc0, 0x63, 0x5d95, 0xb9, 0xc963}, + {0x6dc0, 0x63, 0x5d95, 0x62, 0x79f7}, + + /* Colossal Adventure /JoD (Commodore 64) */ + {0x6c8e, 0xb6, 0x9be3, 0x36, 0x6971}, + {0x6c8e, 0xb6, 0x9be3, 0xb5, 0xeba0}, + + /* Adventure Quest /JoD (Commodore 64) */ + {0x63b6, 0x2e, 0xef38, 0x93, 0x4e68}, + {0x63b6, 0x2e, 0xef38, 0x2d, 0x54dc}, + + /* Dungeon Adventure /JoD (Commodore 64) */ + {0x6bd2, 0x65, 0xa41f, 0xbb, 0x4260}, + {0x6bd2, 0x65, 0xa41f, 0x64, 0xdf5a}, + + /* Colossal Adventure /JoD (Spectrum48) */ + {0x5a8e, 0xf2, 0x7cca, 0x72, 0x8e58}, + {0x5a8e, 0xf2, 0x7cca, 0xf1, 0x0c89}, + {0x5a8e, 0xf2, 0x7cca, 0xf2, 0x2c96}, + + /* Adventure Quest /JoD (Spectrum48) */ + {0x5ace, 0x11, 0xdc12, 0x76, 0x8663}, + {0x5ace, 0x11, 0xdc12, 0x10, 0xa757}, + {0x5ace, 0x11, 0xdc12, 0x11, 0xf118}, + + /* Dungeon Adventure /JoD (Spectrum48) */ + {0x58a3, 0x38, 0x8ce4, 0x8e, 0xb61a}, + {0x58a3, 0x38, 0x8ce4, 0x37, 0x34c0}, + {0x58a3, 0x38, 0x8ce4, 0x38, 0xa1ee}, + + /* Snowball /SD (Amiga/ST) */ + {0x7b31, 0x6e, 0x2e2b, 0xe5, 0x6017}, + + /* Return to Eden /SD (Amiga/ST) */ + {0x7d16, 0xe6, 0x5438, 0x5d, 0xc770}, + + /* Worm in Paradise /SD (Amiga/ST) */ + {0x7cd9, 0x0c, 0x4df1, 0x83, 0xe997}, + + /* Snowball /SD (PC/Spectrum128) */ + {0x7b2f, 0x70, 0x6955, 0xe7, 0x0af4}, + {0x7b2f, 0x70, 0x6955, 0x70, 0x1179}, + + /* Return to Eden /SD (PC) */ + {0x7d14, 0xe8, 0xfbab, 0x5f, 0xeab9}, + {0x7d14, 0xe8, 0xfbab, 0xe8, 0xe216}, + + /* Return to Eden /SD (Amstrad CPC) */ + {0x7cff, 0xf8, 0x6044, 0x6f, 0xbb57}, + + /* Return to Eden /SD (Spectrum128) */ + {0x7c55, 0x18, 0xdaee, 0x8f, 0x01fd}, + + /* Worm in Paradise /SD (Amstrad CPC/PC/Spectrum128) */ + {0x7cd7, 0x0e, 0x4feb, 0x85, 0x4eae}, + {0x7cd7, 0x0e, 0x4feb, 0x0e, 0xb02c}, + + /* Snowball /SD (Commodore 64) */ + {0x7363, 0x65, 0xa0ab, 0xdc, 0xca6a}, + + /* Return to Eden /SD (Commodore 64) */ + {0x772f, 0xca, 0x8602, 0x41, 0x9bd0}, + + /* Worm in Paradise /SD (Commodore 64) */ + {0x788d, 0x72, 0x888a, 0xe9, 0x4cce}, + + /* Snowball /SD (Atari) */ + {0x6bf8, 0x3f, 0xc9f7, 0x96, 0x1908}, + + /* Return to Eden /SD (Atari) */ + {0x60f7, 0x68, 0xc2bc, 0xdf, 0xd3ae}, + + /* Worm in Paradise /SD (Atari) */ + {0x6161, 0xf3, 0xe6d7, 0x6a, 0xe232}, + + /* Snowball /SD (Spectrum48) */ + {0x6541, 0x02, 0x2e6c, 0x79, 0xb80c}, + {0x6541, 0x02, 0x2e6c, 0x02, 0x028a}, + + /* Return to Eden /SD (Spectrum48) */ + {0x5f43, 0xca, 0x828c, 0x41, 0x9f5e}, + {0x5f43, 0xca, 0x828c, 0xca, 0x6e1b}, + + /* Worm in Paradise /SD (Spectrum48) */ + {0x5ebb, 0xf1, 0x4dec, 0x68, 0x4909}, + {0x5ebb, 0xf1, 0x4dec, 0xf1, 0xcc1a}, + + /* Knight Orc, pt. 1 (Amiga) */ + {0xbb93, 0x36, 0x6a05, 0xad, 0xe52d}, + + /* Knight Orc, pt. 1 (ST) */ + {0xbb6e, 0xad, 0x4d40, 0x24, 0x3bcd}, + + /* Knight Orc, pt. 2 (Amiga/ST) */ + {0xc58e, 0x4a, 0x4e9d, 0xc1, 0xe2bf}, + + /* Knight Orc, pt. 3 (Amiga/ST) */ + {0xcb9a, 0x0f, 0x0804, 0x86, 0x6487}, + + /* Knight Orc, pt. 1 (PC) */ + {0xbb6e, 0xa6, 0x9753, 0x1d, 0x2e7f}, + {0xbb6e, 0xa6, 0x9753, 0xa6, 0x001d}, + + /* Knight Orc, pt. 2 (PC) */ + {0xc58e, 0x43, 0xe9ce, 0xba, 0x5e4c}, + {0xc58e, 0x43, 0xe9ce, 0x43, 0xa8f0}, + + /* Knight Orc, pt. 3 (PC) */ + {0xcb9a, 0x08, 0x6c36, 0x7f, 0xf0d4}, + {0xcb9a, 0x08, 0x6c36, 0x08, 0x2d08}, + + /* Knight Orc, pt. 1 (Commodore 64 Gfx) */ + {0x8970, 0x6b, 0x3c7b, 0xe2, 0xb6f3}, + + /* Knight Orc, pt. 1 (Spectrum48) */ + {0x86d0, 0xb7, 0xadbd, 0x2e, 0x43e1}, + + /* Gnome Ranger, pt. 1 (Amiga/ST) */ + {0xb1a9, 0x80, 0x5fb7, 0xf7, 0x5c6c}, + + /* Gnome Ranger, pt. 2 (Amiga/ST) */ + {0xab9d, 0x31, 0xbe6d, 0xa8, 0xcb96}, + + /* Gnome Ranger, pt. 3 (Amiga/ST) */ + {0xae28, 0x87, 0xb6b6, 0xfe, 0x760c}, + + /* Gnome Ranger, pt. 1 (PC) */ + {0xb1aa, 0xad, 0xaf47, 0x24, 0x5cfd}, + {0xb1aa, 0xad, 0xaf47, 0xad, 0xe0ed}, + + /* Gnome Ranger, pt. 1 (ST-var) */ + {0xb19e, 0x92, 0x8f96, 0x09, 0x798c}, + + /* Gnome Ranger, pt. 2 (PC/ST-var) */ + {0xab8b, 0xbf, 0x31e6, 0x36, 0x811c}, + {0xab8b, 0xbf, 0x31e6, 0xbf, 0x8ff3}, + + /* Gnome Ranger, pt. 3 (PC/ST-var) */ + {0xae16, 0x81, 0x8741, 0xf8, 0x47fb}, + {0xae16, 0x81, 0x8741, 0x81, 0xc8eb}, + + /* Gnome Ranger, pt. 1 (Commodore 64 TO) */ + {0xad41, 0xa8, 0x42c5, 0x1f, 0x7d1e}, + + /* Gnome Ranger, pt. 2 (Commodore 64 TO) */ + {0xa735, 0xf7, 0x2e08, 0x6e, 0x780e}, + + /* Gnome Ranger, pt. 3 (Commodore 64 TO) */ + {0xa9c0, 0x9e, 0x0d70, 0x15, 0x3e6b}, + + /* Gnome Ranger, pt. 1 (Commodore 64 Gfx) */ + {0x908e, 0x0d, 0x58a7, 0x84, 0xab1d}, + + /* Gnome Ranger, pt. 2 (Commodore 64 Gfx) */ + {0x8f6f, 0x0a, 0x411a, 0x81, 0x12bc}, + + /* Gnome Ranger, pt. 3 (Commodore 64 Gfx) */ + {0x9060, 0xbb, 0xe75d, 0x32, 0x14e7}, + + /* Lords of Time /T&M (PC) */ + {0xb57c, 0x44, 0x7779, 0xbb, 0x31a6}, + {0xb57c, 0x44, 0x7779, 0x44, 0xea72}, + + /* Red Moon /T&M (PC) */ + {0xa69e, 0x6c, 0xb268, 0xe3, 0x4cef}, + {0xa69e, 0x6c, 0xb268, 0x6c, 0x3799}, + + /* Price of Magik /T&M (PC) */ + {0xbac7, 0x7f, 0xddb2, 0xf6, 0x6ab3}, + {0xbac7, 0x7f, 0xddb2, 0x7f, 0x905c}, + + /* Lords of Time /T&M (ST) */ + {0xb579, 0x89, 0x3e89, 0x00, 0xa2b7}, + + /* Red Moon /T&M (ST) */ + {0xa698, 0x41, 0xcaca, 0xb8, 0xeeac}, + + /* Price of Magik /T&M (ST) */ + {0xbac4, 0x80, 0xa750, 0xf7, 0xe030}, + + /* Lords of Time /T&M (Amiga) */ + {0xb576, 0x2a, 0x7239, 0xa1, 0x2ea6}, + + /* Red Moon /T&M (Amiga) */ + {0xa692, 0xd1, 0x6a99, 0x48, 0x50ff}, + + /* Price of Magik /T&M (Amiga) */ + {0xbaca, 0x3a, 0x221b, 0xb1, 0x55bb}, + + /* Lords of Time /T&M (Commodore 64 TO) */ + {0xb38c, 0x37, 0x9f8e, 0xae, 0xc6b1}, + + /* Red Moon /T&M (Commodore 64 TO) */ + {0xa4e2, 0xa6, 0x016d, 0x1d, 0x31ab}, + + /* Price of Magik /T&M (Commodore 64 TO) */ + {0xb451, 0xa8, 0x2682, 0x1f, 0x5de2}, + + /* Lords of Time /T&M (Commodore 64 Gfx) */ + {0x9070, 0x43, 0x45d4, 0xba, 0x02eb}, + + /* Red Moon /T&M (Commodore 64 Gfx) */ + {0x903f, 0x6b, 0x603e, 0xe2, 0x9f59}, + + /* Price of Magik /T&M (Commodore 64 Gfx) */ + {0x8f51, 0xb2, 0x6c9a, 0x29, 0xde3b}, + + /* Lords of Time /T&M (Spectrum48) */ + {0x8950, 0xa1, 0xbb16, 0x18, 0x2828}, + {0x8950, 0xa1, 0xbb16, 0xa1, 0x1ea2}, + + /* Red Moon /T&M (Spectrum48) */ + {0x8813, 0x11, 0x22de, 0x88, 0x18b8}, + {0x8813, 0x11, 0x22de, 0x11, 0xd0cd}, + + /* Price of Magik /T&M (Spectrum48) */ + {0x8a60, 0x2a, 0x29ed, 0xa1, 0x5e4d}, + + /* Lancelot, pt. 1 (Amiga/PC/ST) */ + {0xc0cf, 0x4e, 0xb7fa, 0xc5, 0x4400}, + + /* Lancelot, pt. 2 (Amiga/PC/ST) */ + {0xd5e9, 0x6a, 0x4192, 0xe1, 0x3b1e}, + + /* Lancelot, pt. 3 (Amiga/PC/ST) */ + {0xbb8f, 0x1a, 0x7487, 0x91, 0x877d}, + + /* Lancelot, pt. 1 (Commodore 64 TO) */ + {0xb4c9, 0x94, 0xd784, 0x0b, 0x203e}, + + /* Lancelot, pt. 2 (Commodore 64 TO) */ + {0xb729, 0x51, 0x8ee5, 0xc8, 0xf1c9}, + + /* Lancelot, pt. 3 (Commodore 64 TO) */ + {0xb702, 0xe4, 0x1809, 0x5b, 0x25b2}, + + /* Lancelot, pt. 1 (Commodore 64 Gfx) */ + {0x8feb, 0xba, 0xa800, 0x31, 0x5bfa}, + + /* Lancelot, pt. 2 (Commodore 64 Gfx) */ + {0x8f6b, 0xfa, 0x0f7e, 0x71, 0x75f2}, + + /* Lancelot, pt. 3 (Commodore 64 Gfx) */ + {0x8f71, 0x2f, 0x0ddc, 0xa6, 0x3e87}, + + /* Ingrid's Back, pt. 1 (PC) */ + {0xd19b, 0xad, 0x306d, 0x24, 0x4504}, + {0xd19b, 0xad, 0x306d, 0xad, 0x878e}, + + /* Ingrid's Back, pt. 2 (PC) */ + {0xc5a5, 0xfe, 0x3c98, 0x75, 0x8950}, + {0xc5a5, 0xfe, 0x3c98, 0xfe, 0x8b7b}, + + /* Ingrid's Back, pt. 3 (PC) */ + {0xd7ae, 0x9e, 0x1878, 0x15, 0xadb0}, + {0xd7ae, 0x9e, 0x1878, 0x9e, 0xaf9b}, + + /* Ingrid's Back, pt. 1 (Amiga) */ + {0xd188, 0x13, 0xdc60, 0x8a, 0x755c}, + + /* Ingrid's Back, pt. 2 (Amiga) */ + {0xc594, 0x03, 0xea95, 0x7a, 0xb5a8}, + + /* Ingrid's Back, pt. 3 (Amiga) */ + {0xd79f, 0xb5, 0x1661, 0x2c, 0xbf5d}, + + /* Ingrid's Back, pt. 1 (ST) */ + {0xd183, 0x83, 0xef72, 0xfa, 0xb04f}, + + /* Ingrid's Back, pt. 2 (ST) */ + {0xc58f, 0x65, 0xf337, 0xdc, 0x900a}, + + /* Ingrid's Back, pt. 3 (ST) */ + {0xd79a, 0x57, 0x49c5, 0xce, 0xe0f9}, + + /* Ingrid's Back, pt. 1 (Commodore 64 TO) */ + {0xb770, 0x03, 0x9a03, 0x7a, 0xdc6a}, + + /* Ingrid's Back, pt. 2 (Commodore 64 TO) */ + {0xb741, 0xb6, 0x2aa5, 0x2d, 0x5a6c}, + + /* Ingrid's Back, pt. 3 (Commodore 64 TO) */ + {0xb791, 0xa1, 0xd065, 0x18, 0xaa0c}, + + /* Ingrid's Back, pt. 1 (Commodore 64 Gfx) */ + {0x9089, 0xce, 0xc5e2, 0x44, 0xeff4}, + + /* Ingrid's Back, pt. 2 (Commodore 64 Gfx) */ + {0x908d, 0x80, 0x30c7, 0xf6, 0x2a11}, + + /* Ingrid's Back, pt. 3 (Commodore 64 Gfx) */ + {0x909e, 0x9f, 0xdecc, 0x15, 0xf4da}, + + {0x0000, 0x00, 0x0000, 0x00, 0x0000}, +}; + + +/* + * gln_gameid_lookup_game() + * gln_gameid_lookup_patch() + * + * Look up and return game table and patch table entries given a game's + * length, checksum, and CRC. Returns the entry, or NULL if not found. + */ +static gln_game_tableref_t +gln_gameid_lookup_game(gln_uint16 length, gln_byte checksum, + gln_uint16 crc, int ignore_crc) { + gln_game_tableref_t game; + + for (game = GLN_GAME_TABLE; game->length; game++) { + if (game->length == length && game->checksum == checksum + && (ignore_crc || game->crc == crc)) + break; + } + + return game->length ? game : NULL; +} + +static gln_patch_tableref_t +gln_gameid_lookup_patch(gln_uint16 length, gln_byte checksum, + gln_uint16 crc) { + gln_patch_tableref_t patch; + + for (patch = GLN_PATCH_TABLE; patch->length; patch++) { + if (patch->length == length && patch->patch_checksum == checksum + && patch->patch_crc == crc) + break; + } + + return patch->length ? patch : NULL; +} + + +/* + * gln_gameid_identify_game() + * + * Identify a game from its data length, checksum, and CRC. Returns the + * entry of the game in the game table, or NULL if not found. + * + * This function uses startdata and FileSize from the core interpreter. + * These aren't advertised symbols, so be warned. + */ +static gln_game_tableref_t +gln_gameid_identify_game(void) { + gln_uint16 length, crc; + gln_byte checksum; + int is_version2; + gln_game_tableref_t game; + gln_patch_tableref_t patch; + + /* If the data file appears too short for a header, give up now. */ + if (FileSize < 30) + return NULL; + + /* + * Find the version of the game, and the length of game data. This logic + * is taken from L9cut, with calcword() replaced by simple byte comparisons. + * If the length exceeds the available data, fail. + */ + assert(startdata); + is_version2 = startdata[4] == 0x20 && startdata[5] == 0x00 + && startdata[10] == 0x00 && startdata[11] == 0x80 + && startdata[20] == startdata[22] + && startdata[21] == startdata[23]; + + length = is_version2 + ? startdata[28] | startdata[29] << BITS_PER_CHAR + : startdata[0] | startdata[1] << BITS_PER_CHAR; + if (length >= FileSize) + return NULL; + + /* Calculate or retrieve the checksum, in a version specific way. */ + if (is_version2) { + int index; + + checksum = 0; + for (index = 0; index < length + 1; index++) + checksum += startdata[index]; + } else + checksum = startdata[length]; + + /* + * Generate a CRC for this data. When L9cut calculates a CRC, it's using a + * copy taken up to length + 1 and then padded with two NUL bytes, so we + * mimic that here. + */ + crc = gln_get_buffer_crc(startdata, length + 1, 2); + + /* + * See if this is a patched file. If it is, look up the game based on the + * original CRC and checksum. If not, use the current CRC and checksum. + */ + patch = gln_gameid_lookup_patch(length, checksum, crc); + game = gln_gameid_lookup_game(length, + patch ? patch->orig_checksum : checksum, + patch ? patch->orig_crc : crc, + FALSE); + + /* If no game identified, retry without the CRC. This is guesswork. */ + if (!game) + game = gln_gameid_lookup_game(length, checksum, crc, TRUE); + + return game; +} + + +/* + * gln_gameid_get_game_name() + * + * Return the name of the game, or NULL if not identifiable. + * + * This function uses startdata from the core interpreter. This isn't an + * advertised symbol, so be warned. + */ +static const char * +gln_gameid_get_game_name(void) { + /* + * If no game name yet known, attempt to identify the game. If it can't + * be identified, set the cached game name to "" -- this special value + * indicates that the game is an unknown one, but suppresses repeated + * attempts to identify it on successive calls. + */ + if (!gln_gameid_game_name) { + gln_game_tableref_t game; + + /* + * If the interpreter hasn't yet loaded a game, startdata is NULL + * (uninitialized, global). In this case, we return NULL, allowing + * for retries until a game is loaded. + */ + if (!startdata) + return NULL; + + game = gln_gameid_identify_game(); + gln_gameid_game_name = game ? game->name : ""; + } + + /* Return the game's name, or NULL if it was unidentifiable. */ + assert(gln_gameid_game_name); + return strlen(gln_gameid_game_name) > 0 ? gln_gameid_game_name : NULL; +} + + +/* + * gln_gameid_game_name_reset() + * + * Clear the saved game name, forcing a new lookup when next queried. This + * function should be called by actions that may cause the interpreter to + * change game file, for example os_set_filenumber(). + */ +static void +gln_gameid_game_name_reset(void) { + gln_gameid_game_name = NULL; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port bitmap picture functions */ +/*---------------------------------------------------------------------*/ + +/* R,G,B color triple definition. */ +typedef struct { + int red, green, blue; +} gln_rgb_t; +typedef gln_rgb_t *gln_rgbref_t; + +/* + * 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 GLN_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 GLN_GRAPHICS_TIMEOUT = 50; + +/* + * Count of timeouts to wait on. 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 GLN_GRAPHICS_REPAINT_WAIT = 10; + +/* Pixel size multiplier for image size scaling. */ +static const int GLN_GRAPHICS_PIXEL = 1; + +/* Proportion of the display to use for graphics. */ +static const glui32 GLN_GRAPHICS_PROPORTION = 30; + +/* + * Special title picture number, requiring its own handling, and count of + * timeouts to wait on after fully rendering the title picture (~2 seconds). + */ +static const int GLN_GRAPHICS_TITLE_PICTURE = 0, + GLN_GRAPHICS_TITLE_WAIT = 40; + +/* + * 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 GLN_GRAPHICS_DEFAULT_BACKGROUND = 0x00ffffff, + GLN_GRAPHICS_BORDER_COLOR = 0x00000000; +static const int GLN_GRAPHICS_BORDER = 1, + GLN_GRAPHICS_SHADING = 2, + GLN_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 GLN_GRAPHICS_UNUSED_PIXEL = 0xff; + +/* Graphics file directory, and type of graphics found in it. */ +static char *gln_graphics_bitmap_directory = NULL; +static BitmapType gln_graphics_bitmap_type = NO_BITMAPS; + +/* The current picture id being displayed. */ +enum { GLN_PALETTE_SIZE = 32 }; +static gln_byte *gln_graphics_bitmap = NULL; +static gln_uint16 gln_graphics_width = 0, + gln_graphics_height = 0; +static Colour gln_graphics_palette[GLN_PALETTE_SIZE]; /* = { 0, ... }; */ +static int gln_graphics_picture = -1; + +/* + * Flags set on new picture, and on resize or arrange events, and a flag + * to indicate whether background repaint is stopped or active. + */ +static int gln_graphics_new_picture = FALSE, + gln_graphics_repaint = FALSE, + gln_graphics_active = FALSE; + +/* + * State to monitor the state of interpreter graphics. The values of the + * enumerations match the modes supplied by os_graphics(). + */ +enum GraphicsState { + GLN_GRAPHICS_OFF = 0, + GLN_GRAPHICS_LINE_MODE = 1, + GLN_GRAPHICS_BITMAP_MODE = 2 +}; +static GraphicsState gln_graphics_interpreter_state = GLN_GRAPHICS_OFF; + + +/* + * 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 gln_byte *gln_graphics_off_screen = NULL, + *gln_graphics_on_screen = NULL; + +/* + * The number of colors used in the palette by the current picture. Because + * of the way it's queried, we risk a race, with admittedly a very low + * probability, with the updater. So, it's initialized instead to the + * largest possible value. The real value in use is inserted on the first + * picture update timeout call for a new picture. + */ +static int gln_graphics_color_count = GLN_PALETTE_SIZE; + + +/* + * gln_graphics_open() + * + * If it's not open, open the graphics window. Returns TRUE if graphics + * was successfully started, or already on. + */ +static int gln_graphics_open(void) { + if (!gln_graphics_window) { + gln_graphics_window = g_vm->glk_window_open(gln_main_window, + winmethod_Above + | winmethod_Proportional, + GLN_GRAPHICS_PROPORTION, + wintype_Graphics, 0); + } + + return gln_graphics_window != NULL; +} + + +/* + * gln_graphics_close() + * + * If open, close the graphics window and set back to NULL. + */ +static void gln_graphics_close(void) { + if (gln_graphics_window) { + g_vm->glk_window_close(gln_graphics_window, NULL); + gln_graphics_window = NULL; + } +} + + +/* + * gln_graphics_start() + * + * If graphics enabled, start any background picture update processing. + */ +static void gln_graphics_start(void) { + if (gln_graphics_enabled) { + /* If not running, start the updating "thread". */ + if (!gln_graphics_active) { + g_vm->glk_request_timer_events(GLN_GRAPHICS_TIMEOUT); + gln_graphics_active = TRUE; + } + } +} + + +/* + * gln_graphics_stop() + * + * Stop any background picture update processing. + */ +static void gln_graphics_stop(void) { + /* If running, stop the updating "thread". */ + if (gln_graphics_active) { + g_vm->glk_request_timer_events(0); + gln_graphics_active = FALSE; + } +} + + +/* + * gln_graphics_are_displayed() + * + * Return TRUE if graphics are currently being displayed, FALSE otherwise. + */ +static int gln_graphics_are_displayed(void) { + return gln_graphics_window != NULL; +} + + +/* + * gln_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 gln_graphics_paint(void) { + if (gln_graphics_enabled && gln_graphics_are_displayed()) { + /* Set the repaint flag, and start graphics. */ + gln_graphics_repaint = TRUE; + gln_graphics_start(); + } +} + + +/* + * gln_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. + */ +static void gln_graphics_restart(void) { + if (gln_graphics_enabled && gln_graphics_are_displayed()) { + /* Set the new picture flag, and start graphics. */ + gln_graphics_new_picture = TRUE; + gln_graphics_start(); + } +} + + +/* + * gln_graphics_count_colors() + * + * Analyze an image, and return an overall count of how many colors out of + * the palette are used. + */ +static int gln_graphics_count_colors(gln_byte bitmap[], + gln_uint16 width, gln_uint16 height) { + int x, y, count; + long usage[GLN_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++; + } + } + + return count; +} + + +/* + * gln_graphics_split_color() + * gln_graphics_combine_color() + * + * General graphics helper functions, to convert between RGB and Glk glui32 + * color representations. + */ +static void gln_graphics_split_color(glui32 color, gln_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 gln_graphics_combine_color(gln_rgbref_t rgb_color) { + glui32 color; + assert(rgb_color); + + color = (rgb_color->red << 16) | (rgb_color->green << 8) | rgb_color->blue; + return color; +} + + +/* + * gln_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 gln_graphics_clear_and_border(winid_t glk_window, + int x_offset, int y_offset, + int pixel_size, gln_uint16 width, + gln_uint16 height) { + uint background; + glui32 fade_color, shading_color; + gln_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(gln_main_window, + style_Normal, stylehint_BackColor, &background)) { + /* + * Unable to get the main window background, so assume, and default + * graphics to white. + */ + background = GLN_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); +#ifndef GARGLK + /* + * 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 * GLN_GRAPHICS_SHADE_STEPS + || height < 2 * GLN_GRAPHICS_SHADE_STEPS) { + /* Paint a rectangle bigger than the picture by border pixels. */ + g_vm->glk_window_fill_rect(glk_window, + GLN_GRAPHICS_BORDER_COLOR, + x_offset - GLN_GRAPHICS_BORDER, + y_offset - GLN_GRAPHICS_BORDER, + width * pixel_size + GLN_GRAPHICS_BORDER * 2, + height * pixel_size + GLN_GRAPHICS_BORDER * 2); + return; + } +#endif + /* + * 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, + GLN_GRAPHICS_BORDER_COLOR, + x_offset - GLN_GRAPHICS_BORDER, + y_offset - GLN_GRAPHICS_BORDER, + width * pixel_size + GLN_GRAPHICS_BORDER * 2 + + GLN_GRAPHICS_SHADING, + height * pixel_size + GLN_GRAPHICS_BORDER * 2 + + GLN_GRAPHICS_SHADING); + + /* + * Split the main window background color and the border color into + * components. + */ + gln_graphics_split_color(background, &rgb_background); + gln_graphics_split_color(GLN_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) + / GLN_GRAPHICS_SHADE_STEPS; + rgb_fade.green = (rgb_background.green - rgb_border.green) + / GLN_GRAPHICS_SHADE_STEPS; + rgb_fade.blue = (rgb_background.blue - rgb_border.blue) + / GLN_GRAPHICS_SHADE_STEPS; + + /* Combine RGB fade into a single incremental Glk color. */ + fade_color = gln_graphics_combine_color(&rgb_fade); + + /* Fade in edge, from background to border, shading in stages. */ + shading_color = background; + for (index = 0; index < GLN_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 + + GLN_GRAPHICS_BORDER, + y_offset + index - GLN_GRAPHICS_BORDER, + GLN_GRAPHICS_SHADING, 1); + g_vm->glk_window_fill_rect(glk_window, shading_color, + x_offset + index - GLN_GRAPHICS_BORDER, + y_offset + height * pixel_size + + GLN_GRAPHICS_BORDER, + 1, GLN_GRAPHICS_SHADING); + + /* Update the shading color for the fade next iteration. */ + shading_color -= fade_color; + } +} + + +/* + * gln_graphics_convert_palette() + * + * Convert a Level 9 bitmap color palette to a Glk one. + */ +static void gln_graphics_convert_palette(Colour ln_palette[], glui32 glk_palette[]) { + int index; + assert(ln_palette && glk_palette); + + for (index = 0; index < GLN_PALETTE_SIZE; index++) { + Colour colour; + gln_rgb_t gln_color; + + /* Convert color from Level 9 to internal RGB, then to Glk color. */ + colour = ln_palette[index]; + gln_color.red = colour.red; + gln_color.green = colour.green; + gln_color.blue = colour.blue; + glk_palette[index] = gln_graphics_combine_color(&gln_color); + } +} + + +/* + * gln_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 gln_graphics_position_picture(winid_t glk_window, int pixel_size, + gln_uint16 width, gln_uint16 height, + int *x_offset, int *y_offset) { + uint 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; +} + + +/* + * gln_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 gln_graphics_is_vertex(gln_byte off_screen[], + gln_uint16 width, gln_uint16 height, int x, int y) { + gln_byte 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 < GLN_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() + * gln_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) 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. + */ +struct gln_layering_t { + long complexity; /* Count of vertices for this color. */ + long usage; /* Color usage count. */ + int color; /* Color index into palette. */ +}; + +static int gln_graphics_compare_layering_inverted(const void *void_first, + const void *void_second) { + const gln_layering_t *first = (const gln_layering_t *)void_first; + const gln_layering_t *second = (const gln_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 gln_graphics_assign_layers(gln_byte off_screen[], gln_byte on_screen[], + gln_uint16 width, gln_uint16 height, + int layers[], long layer_usage[]) { + int index, x, y; + long index_row; + gln_layering_t layering[GLN_PALETTE_SIZE]; + assert(off_screen && on_screen && layers && layer_usage); + + /* Clear initial complexity and usage counts, and set initial colors. */ + for (index = 0; index < GLN_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 idx 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 (gln_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, GLN_PALETTE_SIZE, + sizeof(*layering), gln_graphics_compare_layering_inverted); + + /* + * Assign a layer to each palette color, and also return the layer usage + * for each layer. + */ + for (index = 0; index < GLN_PALETTE_SIZE; index++) { + layers[layering[index].color] = index; + layer_usage[index] = layering[index].usage; + } +} + + +/* + * gln_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 gln_graphics_paint_region(winid_t glk_window, glui32 palette[], int layers[], + gln_byte off_screen[], gln_byte on_screen[], + int x, int y, int x_offset, int y_offset, + int pixel_size, gln_uint16 width, gln_uint16 height) { + gln_byte 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 < GLN_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; + } +} + +static void gln_graphics_paint_everything(winid_t glk_window, + glui32 palette[], + gln_byte off_screen[], + int x_offset, int y_offset, + gln_uint16 width, gln_uint16 height) { + gln_byte 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 * GLN_GRAPHICS_PIXEL + x_offset, + y * GLN_GRAPHICS_PIXEL + y_offset, + GLN_GRAPHICS_PIXEL, GLN_GRAPHICS_PIXEL); + } + } +} + +/* + * gln_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 gln_graphics_timeout(void) { + static glui32 palette[GLN_PALETTE_SIZE]; /* Precomputed Glk palette */ + + 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 */ + static int saved_layer; /* Saved current layer */ + static int saved_x, saved_y; /* Saved x,y coord */ + + static int total_regions; /* Debug statistic */ + + gln_byte *on_screen; /* On-screen image buffer */ + gln_byte *off_screen; /* Off-screen image buffer */ + long picture_size; /* Picture size in pixels */ + + /* Ignore the call if the current graphics state is inactive. */ + if (!gln_graphics_active) + return; + assert(gln_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 (gln_graphics_repaint) { + deferred_repaint = TRUE; + gln_graphics_repaint = FALSE; + ignore_counter = GLN_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 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 = gln_graphics_width * gln_graphics_height; + off_screen = gln_graphics_off_screen; + on_screen = gln_graphics_on_screen; + + /* + * If we received a new picture, set up the local static variables for that + * picture -- convert the color palette, and initialize the off_screen + * buffer to be the base picture. + */ + if (gln_graphics_new_picture) { + /* Initialize the off_screen buffer to be a copy of the base picture. */ + free(off_screen); + off_screen = (gln_byte *)gln_malloc(picture_size * sizeof(*off_screen)); + memcpy(off_screen, gln_graphics_bitmap, + picture_size * sizeof(*off_screen)); + + /* Note the buffer for freeing on cleanup. */ + gln_graphics_off_screen = off_screen; + + /* + * Pre-convert all the picture palette colors into their corresponding + * Glk colors. + */ + gln_graphics_convert_palette(gln_graphics_palette, palette); + + /* Save the color count for possible queries later. */ + gln_graphics_color_count = + gln_graphics_count_colors(off_screen, + gln_graphics_width, gln_graphics_height); + } + + /* + * 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 (gln_graphics_new_picture || deferred_repaint) { + /* + * Calculate the x and y offset to center the picture in the graphics + * window. + */ + gln_graphics_position_picture(gln_graphics_window, + GLN_GRAPHICS_PIXEL, + gln_graphics_width, gln_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 = (gln_byte *)gln_malloc(picture_size * sizeof(*on_screen)); + memset(on_screen, GLN_GRAPHICS_UNUSED_PIXEL, + picture_size * sizeof(*on_screen)); + + /* Note the buffer for freeing on cleanup. */ + gln_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 + gln_graphics_assign_layers(off_screen, on_screen, + gln_graphics_width, gln_graphics_height, + layers, layer_usage); +#endif + + /* Clear the graphics window. */ + gln_graphics_clear_and_border(gln_graphics_window, + x_offset, y_offset, + GLN_GRAPHICS_PIXEL, + gln_graphics_width, gln_graphics_height); + + /* Start a fresh picture rendering pass. */ + yield_counter = 0; + saved_layer = 0; + saved_x = 0; + saved_y = 0; + total_regions = 0; + + /* Clear the new picture and deferred repaint flags. */ + gln_graphics_new_picture = FALSE; + deferred_repaint = FALSE; + } + +#ifndef GARGLK + int layer; /* Image layer iterator */ + int x, y; /* Image iterators */ + int regions; /* Count of regions painted */ + static int layers[GLN_PALETTE_SIZE]; /* Assigned image layers */ + static long layer_usage[GLN_PALETTE_SIZE]; /* Image layer occupancies */ + + /* + * 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 < GLN_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 * gln_graphics_width; + for (y = saved_y; y < gln_graphics_height; y++) { + for (x = saved_x; x < gln_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. + */ + gln_graphics_paint_region(gln_graphics_window, + palette, layers, + off_screen, on_screen, + x, y, x_offset, y_offset, + GLN_GRAPHICS_PIXEL, + gln_graphics_width, + gln_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 >= GLN_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 += gln_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 < GLN_REPAINT_LIMIT); + total_regions += regions; + +#else + gln_graphics_paint_everything + (gln_graphics_window, + palette, off_screen, + x_offset, y_offset, + gln_graphics_width, + gln_graphics_height); +#endif + + /* Stop graphics; there's no more to be done until something restarts us. */ + gln_graphics_stop(); +} + +/* + * gln_graphics_locate_bitmaps() + * + * Given the name of the game file being run, try to set up the graphics + * directory and bitmap type for that game. If none available, set the + * directory to NULL, and bitmap type to NO_BITMAPS. + */ +static void gln_graphics_locate_bitmaps(const char *gamefile) { + const char *basename; + char *dirname; + BitmapType bitmap_type; + + /* Find the start of the last element of the filename passed in. */ + basename = strrchr(gamefile, GLN_FILE_DELIM); + basename = basename ? basename + 1 : gamefile; + + /* Take a copy of the directory part of the filename. */ + dirname = (char *)gln_malloc(basename - gamefile + 1); + strncpy(dirname, gamefile, basename - gamefile); + dirname[basename - gamefile] = '\0'; + + /* + * Use the core interpreter to search for suitable bitmaps. If none found, + * free allocated memory and return noting none available. + */ + bitmap_type = DetectBitmaps(dirname); + if (bitmap_type == NO_BITMAPS) { + free(dirname); + gln_graphics_bitmap_directory = NULL; + gln_graphics_bitmap_type = NO_BITMAPS; + return; + } + + /* Record the bitmap details for later use. */ + gln_graphics_bitmap_directory = dirname; + gln_graphics_bitmap_type = bitmap_type; +} + + +/* + * gln_graphics_handle_title_picture() + * + * Picture 0 is special, normally the title picture. Unless we handle it + * specially, the next picture comes along and instantly overwrites it. + * Here, then, we try to delay until the picture has rendered, allowing the + * delay to be broken with a keypress. + */ +static void gln_graphics_handle_title_picture(void) { + event_t event; + int count; + + gln_standout_string("\n[ Press any key to skip the title picture... ]\n\n"); + + /* Wait until a keypress or graphics rendering is complete. */ + g_vm->glk_request_char_event(gln_main_window); + do { + gln_event_wait_2(evtype_CharInput, evtype_Timer, &event); + + /* + * If a character was pressed, return. This will let the game + * progress, probably into showing the next bitmap. + */ + if (event.type == evtype_CharInput) { + gln_watchdog_tick(); + return; + } + } while (gln_graphics_active); + + /* + * Now wait another couple of seconds, or until a keypress. We'll do this + * in graphics timeout chunks, so that if graphics restarts while we're + * delaying, and it requests timer events and overwrites ours, we wind up + * with the identical timer event period to the one we're expecting anyway. + */ + g_vm->glk_request_timer_events(GLN_GRAPHICS_TIMEOUT); + for (count = 0; count < GLN_GRAPHICS_TITLE_WAIT; count++) { + gln_event_wait_2(evtype_CharInput, evtype_Timer, &event); + + if (event.type == evtype_CharInput) + break; + } + + /* + * While we waited, a Glk arrange or redraw event could have triggered + * graphics into repainting, and using timers. To handle this, stop timers + * only if graphics is inactive. If active, graphics will stop timers + * itself when it finishes rendering. We can't stop timers here while + * graphics is active; that will hang the graphics "thread". + */ + if (!gln_graphics_active) + g_vm->glk_request_timer_events(0); + + /* Cancel possible pending character event, and continue on. */ + g_vm->glk_cancel_char_event(gln_main_window); + gln_watchdog_tick(); +} + + +/* + * os_show_bitmap() + * + * 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. + */ +void os_show_bitmap(int picture, int x, int y) { + Bitmap *bitmap; + long picture_bytes; + + /* + * If interpreter graphics are disabled, the only way we can get into here + * is using #picture. It seems that the interpreter won't always deliver + * correct bitmaps with #picture when in text mode, so it's simplest here + * if we just ignore those calls. + */ + if (gln_graphics_interpreter_state != GLN_GRAPHICS_BITMAP_MODE) + return; + + /* Ignore repeat calls for the currently displayed picture. */ + if (picture == gln_graphics_picture) + return; + + /* + * Get the core interpreter's bitmap for the requested picture. If this + * returns NULL, the picture doesn't exist, so ignore the call silently. + */ + bitmap = DecodeBitmap(gln_graphics_bitmap_directory, + gln_graphics_bitmap_type, picture, x, y); + if (!bitmap) + return; + + /* + * Note the last thing passed to os_show_bitmap, to avoid possible repaints + * of the current picture. + */ + gln_graphics_picture = picture; + + /* Calculate the picture size in bytes. */ + picture_bytes = bitmap->width * bitmap->height * sizeof(*bitmap->bitmap); + + /* + * Save the picture details for the update code. Here we take a complete + * local copy of the bitmap, dimensions, and palette. The core interpreter + * may return a palette with fewer colors than our maximum, so unused local + * palette entries are set to zero. + */ + free(gln_graphics_bitmap); + gln_graphics_bitmap = (gln_byte *)gln_malloc(picture_bytes); + memcpy(gln_graphics_bitmap, bitmap->bitmap, picture_bytes); + gln_graphics_width = bitmap->width; + gln_graphics_height = bitmap->height; + memset(gln_graphics_palette, 0, sizeof(gln_graphics_palette)); + memcpy(gln_graphics_palette, bitmap->palette, + bitmap->npalette * sizeof(bitmap->palette[0])); + + /* + * If graphics are enabled, both at the Glk level and in the core + * interpreter, 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 (gln_graphics_enabled + && gln_graphics_interpreter_state == GLN_GRAPHICS_BITMAP_MODE) { + /* + * Ensure graphics on, then set the new picture flag and start the + * updating "thread". If this is the title picture, start special + * handling. + */ + if (gln_graphics_open()) { + gln_graphics_new_picture = TRUE; + gln_graphics_start(); + + if (picture == GLN_GRAPHICS_TITLE_PICTURE) + gln_graphics_handle_title_picture(); + } + } +} + + +/* + * gln_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 gln_graphics_picture_is_available(void) { + return gln_graphics_bitmap != NULL; +} + + +/* + * gln_graphics_get_picture_details() + * + * Return the width and height 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 gln_graphics_get_picture_details(int *width, int *height) { + if (gln_graphics_picture_is_available()) { + if (width) + *width = gln_graphics_width; + if (height) + *height = gln_graphics_height; + + return TRUE; + } + + return FALSE; +} + + +/* + * gln_graphics_get_rendering_details() + * + * Returns the type of bitmap in use (if any), 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 bitmap type, 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 + * values are returned. + */ +static int gln_graphics_get_rendering_details(const char **bitmap_type, + int *color_count, int *is_active) { + if (gln_graphics_enabled && gln_graphics_are_displayed()) { + /* + * Convert the detected bitmap type into a string and return it. + * A NULL bitmap string implies no bitmaps. + */ + if (bitmap_type) { + const char *return_type; + + switch (gln_graphics_bitmap_type) { + case AMIGA_BITMAPS: + return_type = "Amiga"; + break; + case PC1_BITMAPS: + return_type = "IBM PC(1)"; + break; + case PC2_BITMAPS: + return_type = "IBM PC(2)"; + break; + case C64_BITMAPS: + return_type = "Commodore 64"; + break; + case BBC_BITMAPS: + return_type = "BBC B"; + break; + case CPC_BITMAPS: + return_type = "Amstrad CPC/Spectrum"; + break; + case MAC_BITMAPS: + return_type = "Macintosh"; + break; + case ST1_BITMAPS: + return_type = "Atari ST(1)"; + break; + case ST2_BITMAPS: + return_type = "Atari ST(2)"; + break; + case NO_BITMAPS: + default: + return_type = NULL; + break; + } + + *bitmap_type = return_type; + } + + /* + * Return the color count noted by timeouts on the first timeout + * following a new picture. We might return the one for the prior + * picture. + */ + if (color_count) + *color_count = gln_graphics_color_count; + + /* Return graphics active flag. */ + if (is_active) + *is_active = gln_graphics_active; + + return TRUE; + } + + return FALSE; +} + + +/* + * gln_graphics_interpreter_enabled() + * + * Return TRUE if it looks like interpreter graphics are turned on, FALSE + * otherwise. + */ +static int gln_graphics_interpreter_enabled(void) { + return gln_graphics_interpreter_state != GLN_GRAPHICS_OFF; +} + + +/* + * gln_graphics_cleanup() + * + * Free memory resources allocated by graphics functions. Called on game + * end. + */ +static void gln_graphics_cleanup(void) { + free(gln_graphics_bitmap); + gln_graphics_bitmap = NULL; + free(gln_graphics_off_screen); + gln_graphics_off_screen = NULL; + free(gln_graphics_on_screen); + gln_graphics_on_screen = NULL; + free(gln_graphics_bitmap_directory); + gln_graphics_bitmap_directory = NULL; + + gln_graphics_bitmap_type = NO_BITMAPS; + gln_graphics_picture = -1; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port line drawing picture adapter functions */ +/*---------------------------------------------------------------------*/ + +/* + * Graphics color table. These eight colors are selected into the four- + * color palette by os_setcolour(). The standard Amiga palette is rather + * over-vibrant, so to soften it a bit this table uses non-primary colors. + */ +static const gln_rgb_t GLN_LINEGRAPHICS_COLOR_TABLE[] = { + { 47, 79, 79}, /* DarkSlateGray [Black] */ + {238, 44, 44}, /* Firebrick2 [Red] */ + { 67, 205, 128}, /* SeaGreen3 [Green] */ + {238, 201, 0}, /* Gold2 [Yellow] */ + { 92, 172, 238}, /* SteelBlue2 [Blue] */ + {139, 87, 66}, /* LightSalmon4 [Brown] */ + {175, 238, 238}, /* PaleTurquoise [Cyan] */ + {245, 245, 245}, /* WhiteSmoke [White] */ +}; + +/* + * Structure of a Seed Fill segment entry, and a growable stack-based array + * of segments pending fill. When length exceeds size, size is increased + * and the array grown. + */ +struct gln_linegraphics_segment_t { + int y; /* Segment y coordinate */ + int xl; /* Segment x left hand side coordinate */ + int xr; /* Segment x right hand side coordinate */ + int dy; /* Segment y delta */ +}; + +static gln_linegraphics_segment_t *gln_linegraphics_fill_segments = NULL; +static int gln_linegraphics_fill_segments_allocation = 0, + gln_linegraphics_fill_segments_length = 0; + + +/* + * gln_linegraphics_create_context() + * + * Initialize a new constructed bitmap graphics context for line drawn + * graphics. + */ +static void gln_linegraphics_create_context(void) { + int width, height; + long picture_bytes; + + /* Get the picture size, and calculate the bytes in the bitmap. */ + GetPictureSize(&width, &height); + picture_bytes = width * height * sizeof(*gln_graphics_bitmap); + + /* + * Destroy any current bitmap, and begin a fresh one. Here we set the + * bitmap and the palette to all zeroes; this equates to all black. + */ + free(gln_graphics_bitmap); + gln_graphics_bitmap = (gln_byte *)gln_malloc(picture_bytes); + memset(gln_graphics_bitmap, 0, picture_bytes); + gln_graphics_width = width; + gln_graphics_height = height; + memset(gln_graphics_palette, 0, sizeof(gln_graphics_palette)); + + /* Set graphics picture number to -1; this is not a real game bitmap. */ + gln_graphics_picture = -1; +} + + +/* + * gln_linegraphics_clear_context() + * + * Clear the complete graphical drawing area, setting all pixels to zero, + * and resetting the palette to all black as well. + */ +static void gln_linegraphics_clear_context(void) { + long picture_bytes; + + /* Get the picture size, and zero all bytes in the bitmap. */ + picture_bytes = gln_graphics_width + * gln_graphics_height * sizeof(*gln_graphics_bitmap); + memset(gln_graphics_bitmap, 0, picture_bytes); + + /* Clear palette colors to all black. */ + memset(gln_graphics_palette, 0, sizeof(gln_graphics_palette)); +} + + +/* + * gln_linegraphics_set_palette_color() + * + * Copy the indicated main color table entry into the palette. + */ +static void gln_linegraphics_set_palette_color(int colour, int index) { + const gln_rgb_t *entry; + assert(colour < GLN_PALETTE_SIZE); + assert(index < (int)sizeof(GLN_LINEGRAPHICS_COLOR_TABLE) + / (int)sizeof(GLN_LINEGRAPHICS_COLOR_TABLE[0])); + + /* Copy the color table entry to the constructed game palette. */ + entry = GLN_LINEGRAPHICS_COLOR_TABLE + index; + gln_graphics_palette[colour].red = entry->red; + gln_graphics_palette[colour].green = entry->green; + gln_graphics_palette[colour].blue = entry->blue; +} + + +/* + * gln_linegraphics_get_pixel() + * gln_linegraphics_set_pixel() + * + * Return and set the bitmap pixel at x,y. + */ +static gln_byte gln_linegraphics_get_pixel(int x, int y) { + assert(x >= 0 && x < gln_graphics_width + && y >= 0 && y < gln_graphics_height); + + return gln_graphics_bitmap[y * gln_graphics_width + x]; +} + +static void gln_linegraphics_set_pixel(int x, int y, gln_byte color) { + assert(x >= 0 && x < gln_graphics_width + && y >= 0 && y < gln_graphics_height); + + gln_graphics_bitmap[y * gln_graphics_width + x] = color; +} + + +/* + * gln_linegraphics_plot_clip() + * gln_linegraphics_draw_line_if() + * + * Draw a line from x1,y1 to x2,y2 in colour1, where the existing pixel + * colour is colour2. The function uses Bresenham's algorithm. The second + * function, gln_graphics_plot_clip, is a line drawing helper; it handles + * clipping, and the requirement to plot a point only if it matches colour2. + */ +static void gln_linegraphics_plot_clip(int x, int y, int colour1, int colour2) { + /* + * Clip the plot if the value is outside the context. Otherwise, plot the + * pixel as colour1 if it is currently colour2. + */ + if (x >= 0 && x < gln_graphics_width && y >= 0 && y < gln_graphics_height) { + if (gln_linegraphics_get_pixel(x, y) == colour2) + gln_linegraphics_set_pixel(x, y, colour1); + } +} + +static void gln_linegraphics_draw_line_if(int x1, int y1, int x2, int y2, + int colour1, int colour2) { + int x, y, dx, dy, incx, incy, balance; + + /* Ignore any odd request where there will be no colour changes. */ + if (colour1 == colour2) + return; + + /* Normalize the line into deltas and increments. */ + if (x2 >= x1) { + dx = x2 - x1; + incx = 1; + } else { + dx = x1 - x2; + incx = -1; + } + + if (y2 >= y1) { + dy = y2 - y1; + incy = 1; + } else { + dy = y1 - y2; + incy = -1; + } + + /* Start at x1,y1. */ + x = x1; + y = y1; + + /* Decide on a direction to progress in. */ + if (dx >= dy) { + dy <<= 1; + balance = dy - dx; + dx <<= 1; + + /* Loop until we reach the end point of the line. */ + while (x != x2) { + gln_linegraphics_plot_clip(x, y, colour1, colour2); + if (balance >= 0) { + y += incy; + balance -= dx; + } + balance += dy; + x += incx; + } + gln_linegraphics_plot_clip(x, y, colour1, colour2); + } else { + dx <<= 1; + balance = dx - dy; + dy <<= 1; + + /* Loop until we reach the end point of the line. */ + while (y != y2) { + gln_linegraphics_plot_clip(x, y, colour1, colour2); + if (balance >= 0) { + x += incx; + balance -= dy; + } + balance += dx; + y += incy; + } + gln_linegraphics_plot_clip(x, y, colour1, colour2); + } +} + + +/* + * gln_linegraphics_push_fill_segment() + * gln_linegraphics_pop_fill_segment() + * gln_linegraphics_fill_4way_if() + * + * Area fill algorithm, set a region to colour1 if it is currently set to + * colour2. This function is a derivation of Paul Heckbert's Seed Fill, + * from "Graphics Gems", Academic Press, 1990, which fills 4-connected + * neighbors. + * + * The main modification is to make segment stacks growable, through the + * helper push and pop functions. There is also a small adaptation to + * check explicitly for color2, to meet the Level 9 API. + */ +static void gln_linegraphics_push_fill_segment(int y, int xl, int xr, int dy) { + /* Clip points outside the graphics context. */ + if (!(y + dy < 0 || y + dy >= gln_graphics_height)) { + int length, allocation; + + length = ++gln_linegraphics_fill_segments_length; + allocation = gln_linegraphics_fill_segments_allocation; + + /* Grow the segments stack if required, successively doubling. */ + if (length > allocation) { + size_t bytes; + + allocation = allocation == 0 ? 1 : allocation << 1; + + bytes = allocation * sizeof(*gln_linegraphics_fill_segments); + gln_linegraphics_fill_segments = + (gln_linegraphics_segment_t *)gln_realloc(gln_linegraphics_fill_segments, bytes); + } + + /* Push top of segments stack. */ + gln_linegraphics_fill_segments[length - 1].y = y; + gln_linegraphics_fill_segments[length - 1].xl = xl; + gln_linegraphics_fill_segments[length - 1].xr = xr; + gln_linegraphics_fill_segments[length - 1].dy = dy; + + /* Write back local dimensions copies. */ + gln_linegraphics_fill_segments_length = length; + gln_linegraphics_fill_segments_allocation = allocation; + } +} + +static void gln_linegraphics_pop_fill_segment(int *y, int *xl, int *xr, int *dy) { + int length; + assert(gln_linegraphics_fill_segments_length > 0); + + length = --gln_linegraphics_fill_segments_length; + + /* Pop top of segments stack. */ + *y = gln_linegraphics_fill_segments[length].y; + *xl = gln_linegraphics_fill_segments[length].xl; + *xr = gln_linegraphics_fill_segments[length].xr; + *dy = gln_linegraphics_fill_segments[length].dy; +} + +static void gln_linegraphics_fill_4way_if(int x, int y, int colour1, int colour2) { + /* Ignore any odd request where there will be no colour changes. */ + if (colour1 == colour2) + return; + + /* Clip fill requests to visible graphics region. */ + if (x >= 0 && x < gln_graphics_width && y >= 0 && y < gln_graphics_height) { + int left, x1, x2, dy, x_lo, x_hi; + + /* + * Level 9 API; explicit check for a match against colour2. This also + * covers the standard Seed Fill check that old pixel value should not + * equal colour1, because of the color1 == colour2 comparison above. + */ + if (gln_linegraphics_get_pixel(x, y) != colour2) + return; + + /* + * Set up inclusive window dimension to ease algorithm translation. + * The original worked with inclusive rectangle limits. + */ + x_lo = 0; + x_hi = gln_graphics_width - 1; + + /* + * The first of these is "needed in some cases", the second is the seed + * segment, popped first. + */ + gln_linegraphics_push_fill_segment(y, x, x, 1); + gln_linegraphics_push_fill_segment(y + 1, x, x, -1); + + while (gln_linegraphics_fill_segments_length > 0) { + /* Pop segment off stack and add delta to y coord. */ + gln_linegraphics_pop_fill_segment(&y, &x1, &x2, &dy); + y += dy; + + /* + * Segment of scan line y-dy for x1<=x<=x2 was previously filled, + * now explore adjacent pixels in scan line y. + */ + for (x = x1; + x >= x_lo && gln_linegraphics_get_pixel(x, y) == colour2; + x--) { + gln_linegraphics_set_pixel(x, y, colour1); + } + + if (x >= x1) + goto skip; + + left = x + 1; + if (left < x1) { + /* Leak on left? */ + gln_linegraphics_push_fill_segment(y, left, x1 - 1, -dy); + } + + x = x1 + 1; + do { + for (; + x <= x_hi && gln_linegraphics_get_pixel(x, y) == colour2; + x++) { + gln_linegraphics_set_pixel(x, y, colour1); + } + + gln_linegraphics_push_fill_segment(y, left, x - 1, dy); + + if (x > x2 + 1) { + /* Leak on right? */ + gln_linegraphics_push_fill_segment(y, x2 + 1, x - 1, -dy); + } + +skip: + for (x++; + x <= x2 && gln_linegraphics_get_pixel(x, y) != colour2; + x++) + ; + + left = x; + } while (x <= x2); + } + } +} + + +/* + * os_cleargraphics() + * os_setcolour() + * os_drawline() + * os_fill() + * + * Interpreter entry points for line drawing graphics. All calls to these + * are ignored if line drawing mode is not set. + */ +void os_cleargraphics(void) { + if (gln_graphics_interpreter_state == GLN_GRAPHICS_LINE_MODE) + gln_linegraphics_clear_context(); +} + +void os_setcolour(int colour, int index) { + if (gln_graphics_interpreter_state == GLN_GRAPHICS_LINE_MODE) + gln_linegraphics_set_palette_color(colour, index); +} + +void os_drawline(int x1, int y1, int x2, int y2, int colour1, int colour2) { + if (gln_graphics_interpreter_state == GLN_GRAPHICS_LINE_MODE) + gln_linegraphics_draw_line_if(x1, y1, x2, y2, colour1, colour2); +} + +void os_fill(int x, int y, int colour1, int colour2) { + if (gln_graphics_interpreter_state == GLN_GRAPHICS_LINE_MODE) + gln_linegraphics_fill_4way_if(x, y, colour1, colour2); +} + + +/* + * gln_linegraphics_process() + * + * Process as many graphics opcodes as are available, constructing the + * resulting image as a bitmap. When complete, treat as normal bitmaps. + */ +static void gln_linegraphics_process(void) { + /* + * If interpreter graphics are not set to line mode, ignore any call that + * arrives here. + */ + if (gln_graphics_interpreter_state == GLN_GRAPHICS_LINE_MODE) { + int opcodes_count; + + /* Run all the available graphics opcodes. */ + for (opcodes_count = 0; RunGraphics();) { + opcodes_count++; + g_vm->glk_tick(); + } + + /* + * If graphics is enabled and we created an image with graphics + * opcodes above, open a graphics window and start bitmap display. + */ + if (gln_graphics_enabled && opcodes_count > 0) { + if (gln_graphics_open()) { + /* Set the new picture flag, and start the updating "thread". */ + gln_graphics_new_picture = TRUE; + gln_graphics_start(); + } + } + } +} + + +/* + * gln_linegraphics_cleanup() + * + * Free memory resources allocated by line graphics functions. Called on + * game end. + */ +static void gln_linegraphics_cleanup(void) { + free(gln_linegraphics_fill_segments); + gln_linegraphics_fill_segments = NULL; + + gln_linegraphics_fill_segments_allocation = 0; + gln_linegraphics_fill_segments_length = 0; +} + + +/*---------------------------------------------------------------------*/ +/* Glk picture dispatch (bitmap or line), and timer arbitration */ +/*---------------------------------------------------------------------*/ + +/* + * Note of the current set graphics mode, to detect changes in mode from + * the core interpreter. + */ +static int gln_graphics_current_mode = -1; + +/* Note indicating if the graphics "thread" is temporarily suspended. */ +static int gln_graphics_suspended = FALSE; + + +/* + * os_graphics() + * + * Called by the main interpreter to turn graphics on and off. Mode 0 + * turns graphics off, mode 1 is line drawing graphics, and mode 2 is + * bitmap graphics. + * + * This function tracks the current state of interpreter graphics setting + * using gln_graphics_interpreter_state. + */ +void os_graphics(int mode) { + /* Ignore the call unless it changes the graphics mode. */ + if (mode != gln_graphics_current_mode) { + /* Set tracked interpreter state given the input mode. */ + switch (mode) { + case 0: + gln_graphics_interpreter_state = GLN_GRAPHICS_OFF; + break; + + case 1: + gln_graphics_interpreter_state = GLN_GRAPHICS_LINE_MODE; + break; + + case 2: + /* If no graphics bitmaps were detected, ignore this call. */ + if (!gln_graphics_bitmap_directory + || gln_graphics_bitmap_type == NO_BITMAPS) + return; + + gln_graphics_interpreter_state = GLN_GRAPHICS_BITMAP_MODE; + break; + } + + /* Given the interpreter state, update graphics activities. */ + switch (gln_graphics_interpreter_state) { + case GLN_GRAPHICS_OFF: + + /* If currently displaying graphics, stop and close window. */ + if (gln_graphics_enabled && gln_graphics_are_displayed()) { + gln_graphics_stop(); + gln_graphics_close(); + } + break; + + case GLN_GRAPHICS_LINE_MODE: + case GLN_GRAPHICS_BITMAP_MODE: + + /* Create a new graphics context on switch to line mode. */ + if (gln_graphics_interpreter_state == GLN_GRAPHICS_LINE_MODE) + gln_linegraphics_create_context(); + + /* + * If we have a picture loaded already, restart graphics. If not, + * we'll delay this until one is supplied by a call to + * os_show_bitmap(). + */ + if (gln_graphics_enabled && gln_graphics_bitmap) { + if (gln_graphics_open()) + gln_graphics_restart(); + } + break; + } + + /* Note the current mode so changes can be detected. */ + gln_graphics_current_mode = mode; + } +} + + +/* + * gln_arbitrate_request_timer_events() + * + * Shim function for g_vm->glk_request_timer_events(), this function should be + * called by other functional areas in place of the main timer event setting + * function. It suspends graphics if busy when setting timer events, and + * resumes graphics if necessary when clearing timer events. + * + * On resuming, it calls the graphics timeout function to simulate the + * timeout that has (probably) been missed. This also ensures that tight + * loops that enable then disable timers using this function don't lock out + * the graphics completely. + * + * Use only in paired calls, the first non-zero, the second zero, and use + * no graphics functions between calls. + */ +static void gln_arbitrate_request_timer_events(glui32 millisecs) { + if (millisecs > 0) { + /* Setting timer events; suspend graphics if currently active. */ + if (gln_graphics_active) { + gln_graphics_suspended = TRUE; + gln_graphics_stop(); + } + + /* Set timer events as requested. */ + g_vm->glk_request_timer_events(millisecs); + } else { + /* + * Resume graphics if currently suspended, otherwise cancel timer + * events as requested by the caller. + */ + if (gln_graphics_suspended) { + gln_graphics_suspended = FALSE; + gln_graphics_start(); + + /* Simulate the "missed" graphics timeout. */ + gln_graphics_timeout(); + } else + g_vm->glk_request_timer_events(0); + } +} + + +/*---------------------------------------------------------------------*/ +/* Glk port infinite loop detection functions */ +/*---------------------------------------------------------------------*/ + +/* Short timeout to wait purely in order to get the display updated. */ +static const glui32 GLN_WATCHDOG_FIXUP = 50; + +/* + * Timestamp of the last watchdog tick call, and timeout. This is used to + * monitor the elapsed time since the interpreter made an I/O call. If it + * remains silent for long enough, set by the timeout, we'll offer the + * option to end the game. A timeout of zero disables the watchdog. + */ +static time_t gln_watchdog_monitor = 0; +static double gln_watchdog_timeout_secs = 0.0; + +/* + * To save thrashing in time(), we want to check for timeouts less frequently + * than we're polled. Here's the control for that. + */ +static int gln_watchdog_check_period = 0, + gln_watchdog_check_counter = 0; + + +/* + * gln_watchdog_start() + * gln_watchdog_stop() + * + * Start and stop watchdog monitoring. + */ +static void gln_watchdog_start(int timeout, int period) { + assert(timeout > 0 && period > 0); + + gln_watchdog_timeout_secs = (double) timeout; + gln_watchdog_check_period = period; + gln_watchdog_check_counter = period; + gln_watchdog_monitor = g_system->getMillis(); +} + +static void gln_watchdog_stop(void) { + gln_watchdog_timeout_secs = 0; +} + + +/* + * gln_watchdog_tick() + * + * Set the watchdog timestamp to the current system time. + * + * This function should be called just before almost every os_* function + * returns to the interpreter, as a means of timing how long the interpreter + * dwells in running game code. + */ +static void gln_watchdog_tick(void) { + gln_watchdog_monitor = g_system->getMillis(); +} + + +/* + * gln_watchdog_has_timed_out() + * + * Check to see if too much time has elapsed since the last tick. If it has, + * offer the option to stop the game, and if accepted, return TRUE. Otherwise, + * if no timeout, or if the watchdog is disabled, return FALSE. + * + * This function only checks every N calls; it's called extremely frequently + * from opcode handling, and will thrash in time() if it checks on each call. + */ +static int gln_watchdog_has_timed_out(void) { + /* If loop detection is off or the timeout is set to zero, do nothing. */ + if (gln_loopcheck_enabled && gln_watchdog_timeout_secs > 0) { + time_t now; + double delta_time; + + /* + * Wait until we've seen enough calls to make a timeout check. If we + * haven't, return FALSE, otherwise reset the counter and continue. + */ + if (--gln_watchdog_check_counter > 0) + return FALSE; + else + gln_watchdog_check_counter = gln_watchdog_check_period; + + /* + * Determine how much time has passed, and offer to end the game if it + * exceeds the allowable timeout. + */ + now = g_system->getMillis(); + delta_time = (now - gln_watchdog_monitor) / 1000; + + if (delta_time >= gln_watchdog_timeout_secs) { + if (gln_confirm("\nThe game may be in an infinite loop. Do you" + " want to stop it? [Y or N] ")) { + gln_watchdog_tick(); + return TRUE; + } + + /* + * If we have timers, set a really short timeout and let it expire. + * This is to force a display update with the response of the + * confirm -- without this, we may not get a screen update for a + * while since at this point the game isn't, by definition, doing + * any input or output. If we don't have timers, no biggie. + */ + if (g_vm->glk_gestalt(gestalt_Timer, 0)) { + event_t event; + + gln_arbitrate_request_timer_events(GLN_WATCHDOG_FIXUP); + gln_event_wait(evtype_Timer, &event); + gln_arbitrate_request_timer_events(0); + } + + /* Reset the monitor and drop into FALSE return -- stop rejected. */ + gln_watchdog_tick(); + } + } + + /* No timeout indicated, or offer rejected by the user. */ + return FALSE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port status line functions */ +/*---------------------------------------------------------------------*/ + +/* Default width used for non-windowing Glk status lines. */ +static const int GLN_DEFAULT_STATUS_WIDTH = 74; + + +/* + * gln_status_update() + * + * Update the information in the status window with the current contents of + * the current game identity string, or a default string if no game identity + * could be established. + */ +static void gln_status_update(void) { + uint width, height; + assert(gln_status_window); + + g_vm->glk_window_get_size(gln_status_window, &width, &height); + if (height > 0) { + const char *game_name; + + g_vm->glk_window_clear(gln_status_window); + g_vm->glk_window_move_cursor(gln_status_window, 0, 0); + g_vm->glk_set_window(gln_status_window); + + /* + * Try to establish a game identity to display; if none, use a standard + * message instead. + */ + game_name = gln_gameid_get_game_name(); + g_vm->glk_put_string(game_name ? game_name : "ScummVM GLK Level 9 Game"); + + g_vm->glk_set_window(gln_main_window); + } +} + + +/* + * gln_status_print() + * + * Print the current contents of the game identity out in the main window, + * if it has changed since the last call. This is for non-windowing Glk + * libraries. + * + * To save memory management hassles, this function uses the CRC functions + * to detect changes of game identity string, and gambles a little on the + * belief that two games' strings won't have the same CRC. + */ +static void gln_status_print(void) { + static int is_initialized = FALSE; + static gln_uint16 crc = 0; + + const char *game_name; + + /* Get the current game name, and do nothing if none available. */ + game_name = gln_gameid_get_game_name(); + if (game_name) { + gln_uint16 new_crc; + + /* + * If not the first call and the game identity string has not changed, + * again, do nothing. + */ + new_crc = gln_get_buffer_crc(game_name, strlen(game_name), 0); + if (!is_initialized || new_crc != crc) { + int index; + +#ifndef GARGLK + /* Set fixed width font to try to preserve status line formatting. */ + g_vm->glk_set_style(style_Preformatted); +#endif + + /* Bracket, and output the extracted game name. */ + g_vm->glk_put_string("[ "); + g_vm->glk_put_string((char *) game_name); + + for (index = strlen(game_name); + index <= GLN_DEFAULT_STATUS_WIDTH; index++) + g_vm->glk_put_char(' '); + g_vm->glk_put_string(" ]\n"); + + crc = new_crc; + is_initialized = TRUE; + } + } +} + + +/* + * gln_status_notify() + * + * Front end function for updating status. Either updates the status window + * or prints the status line to the main window. + */ +static void gln_status_notify(void) { + if (gln_status_window) + gln_status_update(); + else + gln_status_print(); +} + + +/* + * gln_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 gln_status_redraw(void) { + if (gln_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(gln_status_window); + g_vm->glk_window_set_arrangement(parent, + winmethod_Above | winmethod_Fixed, 1, NULL); + + gln_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 gln_help_requested = FALSE, + gln_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 *gln_output_buffer = NULL; +static int gln_output_allocation = 0, + gln_output_length = 0; + +/* + * Output activity flag. Set when os_printchar() is called, and queried + * periodically by os_readchar(). Helps os_readchar() judge whether it must + * request input, or when it's being used as a crude scroll control. + */ +static int gln_output_activity = FALSE; + +/* + * Flag to indicate if the last buffer flushed looked like it ended in a + * "> " prompt. Some later games switch to this mode after a while, and + * it's nice not to duplicate this prompt with our own. + */ +static int gln_output_prompt = FALSE; + + +/* + * gln_output_notify() + * + * Register recent text output from the interpreter. This function is + * called by os_printchar(). + */ +static void gln_output_notify(void) { + gln_output_activity = TRUE; +} + + +/* + * gln_recent_output() + * + * Return TRUE if the interpreter has recently output text, FALSE otherwise. + * Clears the flag, so that more output text is required before the next + * call returns TRUE. + */ +static int gln_recent_output(void) { + int result; + + result = gln_output_activity; + gln_output_activity = FALSE; + + return result; +} + + +/* + * gln_output_register_help_request() + * gln_output_silence_help_hints() + * gln_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 gln_output_register_help_request(void) { + gln_help_requested = TRUE; +} + +static void gln_output_silence_help_hints(void) { + gln_help_hints_silenced = TRUE; +} + +static void gln_output_provide_help_hint(void) { + if (gln_help_requested && !gln_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"); + + gln_help_requested = FALSE; + g_vm->glk_set_style(style_Normal); + } +} + + +/* + * gln_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 gln_game_prompted(void) { + int result; + + result = gln_output_prompt; + gln_output_prompt = FALSE; + + return result; +} + + +/* + * gln_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 gln_detect_game_prompt(void) { + int index; + + gln_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 = gln_output_length - 1; + index >= 0 && gln_output_buffer[index] != '\n'; index--) { + if (gln_output_buffer[index] != ' ') { + gln_output_prompt = TRUE; + break; + } + } +} + + +/* + * gln_output_delete() + * + * Delete all buffered output text. Free all malloc'ed buffer memory, and + * return the buffer variables to their initial values. + */ +static void gln_output_delete(void) { + free(gln_output_buffer); + gln_output_buffer = NULL; + gln_output_allocation = gln_output_length = 0; +} + + +/* + * gln_output_flush() + * + * Flush any buffered output text to the Glk main window, and clear the + * buffer. Check in passing for game prompts that duplicate our's. + */ +static void gln_output_flush(void) { + assert(g_vm->glk_stream_get_current()); + + if (gln_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). + */ + gln_detect_game_prompt(); + + if (gln_output_prompt) { + int index; + + for (index = gln_output_length - 1; + index >= 0 && gln_output_buffer[index] != '\n';) + index--; + + g_vm->glk_put_buffer(gln_output_buffer, index + 1); + gln_output_provide_help_hint(); + g_vm->glk_put_buffer(gln_output_buffer + index + 1, + gln_output_length - index - 1); + } else { + g_vm->glk_put_buffer(gln_output_buffer, gln_output_length); + gln_output_provide_help_hint(); + } + + gln_output_delete(); + } +} + + +/* + * os_printchar() + * + * Buffer a character for eventual printing to the main window. + */ +void os_printchar(char c) { + int bytes; + assert(gln_output_length <= gln_output_allocation); + + /* Grow the output buffer if necessary. */ + for (bytes = gln_output_allocation; bytes < gln_output_length + 1;) + bytes = bytes == 0 ? 1 : bytes << 1; + + if (bytes > gln_output_allocation) { + gln_output_buffer = (char *)gln_realloc(gln_output_buffer, bytes); + gln_output_allocation = bytes; + } + + /* + * Add the character to the buffer, handling return as a newline, and + * note that the game created some output. + */ + gln_output_buffer[gln_output_length++] = (c == '\r' ? '\n' : c); + gln_output_notify(); +} + + +/* + * gln_styled_string() + * gln_styled_char() + * gln_standout_string() + * gln_standout_char() + * gln_normal_string() + * gln_normal_char() + * gln_header_string() + * gln_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 gln_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 gln_styled_char(glui32 style, char c) { + char buffer[2]; + + buffer[0] = c; + buffer[1] = '\0'; + gln_styled_string(style, buffer); +} + +static void gln_standout_string(const char *message) { + gln_styled_string(style_Emphasized, message); +} + +static void gln_standout_char(char c) { + gln_styled_char(style_Emphasized, c); +} + +static void gln_normal_string(const char *message) { + gln_styled_string(style_Normal, message); +} + +static void gln_normal_char(char c) { + gln_styled_char(style_Normal, c); +} + +static void gln_header_string(const char *message) { + gln_styled_string(style_Header, message); +} + +static void gln_banner_string(const char *message) { + gln_styled_string(style_Subheader, message); +} + + +/* + * os_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 g_vm->glk_output_flush() when reading line or + * character input. + * + * Taking os_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 os_flush(void) { +} + + +/*---------------------------------------------------------------------*/ +/* Glk command escape functions */ +/*---------------------------------------------------------------------*/ + +/* + * gln_command_script() + * + * Turn game output scripting (logging) on and off. + */ +static void gln_command_script(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gln_transcript_stream) { + gln_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) { + gln_standout_string("Glk transcript failed.\n"); + return; + } + + gln_transcript_stream = g_vm->glk_stream_open_file(fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gln_transcript_stream) { + gln_standout_string("Glk transcript failed.\n"); + return; + } + + g_vm->glk_window_set_echo_stream(gln_main_window, gln_transcript_stream); + + gln_normal_string("Glk transcript is now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_transcript_stream) { + gln_normal_string("Glk transcript is already off.\n"); + return; + } + + g_vm->glk_stream_close(gln_transcript_stream, NULL); + gln_transcript_stream = NULL; + + g_vm->glk_window_set_echo_stream(gln_main_window, NULL); + + gln_normal_string("Glk transcript is now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk transcript is "); + gln_normal_string(gln_transcript_stream ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk transcript can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_inputlog() + * + * Turn game input logging on and off. + */ +static void gln_command_inputlog(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gln_inputlog_stream) { + gln_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) { + gln_standout_string("Glk input logging failed.\n"); + return; + } + + gln_inputlog_stream = g_vm->glk_stream_open_file(fileref, + filemode_WriteAppend, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gln_inputlog_stream) { + gln_standout_string("Glk input logging failed.\n"); + return; + } + + gln_normal_string("Glk input logging is now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_inputlog_stream) { + gln_normal_string("Glk input logging is already off.\n"); + return; + } + + g_vm->glk_stream_close(gln_inputlog_stream, NULL); + gln_inputlog_stream = NULL; + + gln_normal_string("Glk input log is now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk input logging is "); + gln_normal_string(gln_inputlog_stream ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk input logging can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_readlog() + * + * Set the game input log, to read input from a file. + */ +static void gln_command_readlog(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + frefid_t fileref; + + if (gln_readlog_stream) { + gln_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) { + gln_standout_string("Glk read log failed.\n"); + return; + } + + if (!g_vm->glk_fileref_does_file_exist(fileref)) { + g_vm->glk_fileref_destroy(fileref); + gln_standout_string("Glk read log failed.\n"); + return; + } + + gln_readlog_stream = g_vm->glk_stream_open_file(fileref, filemode_Read, 0); + g_vm->glk_fileref_destroy(fileref); + if (!gln_readlog_stream) { + gln_standout_string("Glk read log failed.\n"); + return; + } + + gln_normal_string("Glk read log is now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_readlog_stream) { + gln_normal_string("Glk read log is already off.\n"); + return; + } + + g_vm->glk_stream_close(gln_readlog_stream, NULL); + gln_readlog_stream = NULL; + + gln_normal_string("Glk read log is now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk read log is "); + gln_normal_string(gln_readlog_stream ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk read log can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_abbreviations() + * + * Turn abbreviation expansions on and off. + */ +static void gln_command_abbreviations(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + if (gln_abbreviations_enabled) { + gln_normal_string("Glk abbreviation expansions are already on.\n"); + return; + } + + gln_abbreviations_enabled = TRUE; + gln_normal_string("Glk abbreviation expansions are now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_abbreviations_enabled) { + gln_normal_string("Glk abbreviation expansions are already off.\n"); + return; + } + + gln_abbreviations_enabled = FALSE; + gln_normal_string("Glk abbreviation expansions are now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk abbreviation expansions are "); + gln_normal_string(gln_abbreviations_enabled ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk abbreviation expansions can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_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 gln_command_graphics(const char *argument) { + assert(argument); + + if (!gln_graphics_possible) { + gln_normal_string("Glk graphics are not available.\n"); + return; + } + + if (gln_strcasecmp(argument, "on") == 0) { + if (gln_graphics_enabled) { + gln_normal_string("Glk graphics are already on.\n"); + return; + } + + gln_graphics_enabled = TRUE; + + /* If a picture is loaded, call the restart function to repaint it. */ + if (gln_graphics_picture_is_available()) { + if (!gln_graphics_open()) { + gln_normal_string("Glk graphics error.\n"); + return; + } + gln_graphics_restart(); + } + + gln_normal_string("Glk graphics are now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_graphics_enabled) { + gln_normal_string("Glk graphics are already off.\n"); + return; + } + + /* + * Set graphics to disabled, and stop any graphics processing. Close + * the graphics window. + */ + gln_graphics_enabled = FALSE; + gln_graphics_stop(); + gln_graphics_close(); + + gln_normal_string("Glk graphics are now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk graphics are available,"); + gln_normal_string(gln_graphics_enabled + ? " and enabled.\n" : " but disabled.\n"); + + if (gln_graphics_picture_is_available()) { + int width, height; + + if (gln_graphics_get_picture_details(&width, &height)) { + char buffer[16]; + + gln_normal_string("There is a picture loaded, "); + + sprintf(buffer, "%d", width); + gln_normal_string(buffer); + gln_normal_string(" by "); + + sprintf(buffer, "%d", height); + gln_normal_string(buffer); + + gln_normal_string(" pixels.\n"); + } + } + + if (!gln_graphics_interpreter_enabled()) + gln_normal_string("Interpreter graphics are disabled.\n"); + + if (gln_graphics_enabled && gln_graphics_are_displayed()) { + const char *bitmap_type; + int color_count, is_active; + + if (gln_graphics_get_rendering_details(&bitmap_type, + &color_count, &is_active)) { + char buffer[16]; + + gln_normal_string("Graphics are "); + gln_normal_string(is_active ? "active, " : "displayed, "); + + sprintf(buffer, "%d", color_count); + gln_normal_string(buffer); + gln_normal_string(" colours"); + + if (bitmap_type) { + gln_normal_string(", "); + gln_normal_string(bitmap_type); + gln_normal_string(" bitmaps"); + } + gln_normal_string(".\n"); + } else + gln_normal_string("Graphics are being displayed.\n"); + } + + if (gln_graphics_enabled && !gln_graphics_are_displayed()) + gln_normal_string("Graphics are not being displayed.\n"); + } + + else { + gln_normal_string("Glk graphics can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_loopchecks() + * + * Turn loop checking (for game infinite loops) on and off. + */ +static void gln_command_loopchecks(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + if (gln_loopcheck_enabled) { + gln_normal_string("Glk loop detection is already on.\n"); + return; + } + + gln_loopcheck_enabled = TRUE; + gln_normal_string("Glk loop detection is now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_loopcheck_enabled) { + gln_normal_string("Glk loop detection is already off.\n"); + return; + } + + gln_loopcheck_enabled = FALSE; + gln_normal_string("Glk loop detection is now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk loop detection is "); + gln_normal_string(gln_loopcheck_enabled ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk loop detection can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_locals() + * + * Turn local interpretation of "quit" etc. on and off. + */ +static void gln_command_locals(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + if (gln_intercept_enabled) { + gln_normal_string("Glk local commands are already on.\n"); + return; + } + + gln_intercept_enabled = TRUE; + gln_normal_string("Glk local commands are now on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_intercept_enabled) { + gln_normal_string("Glk local commands are already off.\n"); + return; + } + + gln_intercept_enabled = FALSE; + gln_normal_string("Glk local commands are now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk local commands are "); + gln_normal_string(gln_intercept_enabled ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk local commands can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_prompts() + * + * Turn the extra "> " prompt output on and off. + */ +static void gln_command_prompts(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + if (gln_prompt_enabled) { + gln_normal_string("Glk extra prompts are already on.\n"); + return; + } + + gln_prompt_enabled = TRUE; + gln_normal_string("Glk extra prompts are now on.\n"); + + /* Check for a game prompt to clear the flag. */ + gln_game_prompted(); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + if (!gln_prompt_enabled) { + gln_normal_string("Glk extra prompts are already off.\n"); + return; + } + + gln_prompt_enabled = FALSE; + gln_normal_string("Glk extra prompts are now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk extra prompts are "); + gln_normal_string(gln_prompt_enabled ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk extra prompts can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_normal_string(".\n"); + } +} + + +/* + * gln_command_print_version_number() + * gln_command_version() + * + * Print out the Glk library version number. + */ +static void gln_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); + gln_normal_string(buffer); +} + +static void gln_command_version(const char *argument) { + glui32 version; + assert(argument); + + gln_normal_string("This is version "); + gln_command_print_version_number(GLN_PORT_VERSION); + gln_normal_string(" of the Glk Level 9 port.\n"); + + version = g_vm->glk_gestalt(gestalt_Version, 0); + gln_normal_string("The Glk library version is "); + gln_command_print_version_number(version); + gln_normal_string(".\n"); +} + + +/* + * gln_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 gln_command_commands(const char *argument) { + assert(argument); + + if (gln_strcasecmp(argument, "on") == 0) { + gln_normal_string("Glk commands are already on.\n"); + } + + else if (gln_strcasecmp(argument, "off") == 0) { + gln_commands_enabled = FALSE; + gln_normal_string("Glk commands are now off.\n"); + } + + else if (strlen(argument) == 0) { + gln_normal_string("Glk commands are "); + gln_normal_string(gln_commands_enabled ? "on" : "off"); + gln_normal_string(".\n"); + } + + else { + gln_normal_string("Glk commands can be "); + gln_standout_string("on"); + gln_normal_string(", or "); + gln_standout_string("off"); + gln_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. */ +} gln_command_t; +typedef gln_command_t *gln_commandref_t; + +static void gln_command_summary(const char *argument); +static void gln_command_help(const char *argument); + +static gln_command_t GLN_COMMAND_TABLE[] = { + {"summary", gln_command_summary, FALSE}, + {"script", gln_command_script, TRUE}, + {"inputlog", gln_command_inputlog, TRUE}, + {"readlog", gln_command_readlog, TRUE}, + {"abbreviations", gln_command_abbreviations, TRUE}, + {"graphics", gln_command_graphics, TRUE}, + {"loopchecks", gln_command_loopchecks, TRUE}, + {"locals", gln_command_locals, TRUE}, + {"prompts", gln_command_prompts, TRUE}, + {"version", gln_command_version, FALSE}, + {"commands", gln_command_commands, TRUE}, + {"help", gln_command_help, TRUE}, + {NULL, NULL, FALSE} +}; + + +/* + * gln_command_summary() + * + * Report all current Glk settings. + */ +static void +gln_command_summary(const char *argument) { + gln_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 = GLN_COMMAND_TABLE; entry->command; entry++) { + if (entry->handler == gln_command_summary + || entry->handler == gln_command_help) + continue; + + entry->handler(""); + } +} + + +/* + * gln_command_help() + * + * Document the available Glk commands. + */ +static void +gln_command_help(const char *command) { + gln_commandref_t entry, matched; + assert(command); + + if (strlen(command) == 0) { + gln_normal_string("Glk commands are"); + for (entry = GLN_COMMAND_TABLE; entry->command; entry++) { + gln_commandref_t next; + + next = entry + 1; + gln_normal_string(next->command ? " " : " and "); + gln_standout_string(entry->command); + gln_normal_string(next->command ? "," : ".\n\n"); + } + + gln_normal_string("Glk commands may be abbreviated, as long as" + " the abbreviation is unambiguous. Use "); + gln_standout_string("glk help"); + gln_normal_string(" followed by a Glk command name for help on that" + " command.\n"); + return; + } + + matched = NULL; + for (entry = GLN_COMMAND_TABLE; entry->command; entry++) { + if (gln_strncasecmp(command, entry->command, strlen(command)) == 0) { + if (matched) { + gln_normal_string("The Glk command "); + gln_standout_string(command); + gln_normal_string(" is ambiguous. Try "); + gln_standout_string("glk help"); + gln_normal_string(" for more information.\n"); + return; + } + matched = entry; + } + } + if (!matched) { + gln_normal_string("The Glk command "); + gln_standout_string(command); + gln_normal_string(" is not valid. Try "); + gln_standout_string("glk help"); + gln_normal_string(" for more information.\n"); + return; + } + + if (matched->handler == gln_command_summary) { + gln_normal_string("Prints a summary of all the current Glk Level 9" + " settings.\n"); + } + + else if (matched->handler == gln_command_script) { + gln_normal_string("Logs the game's output to a file.\n\nUse "); + gln_standout_string("glk script on"); + gln_normal_string(" to begin logging game output, and "); + gln_standout_string("glk script off"); + gln_normal_string(" to end it. Glk Level 9 will ask you for a file" + " when you turn scripts on.\n"); + } + + else if (matched->handler == gln_command_inputlog) { + gln_normal_string("Records the commands you type into a game.\n\nUse "); + gln_standout_string("glk inputlog on"); + gln_normal_string(", to begin recording your commands, and "); + gln_standout_string("glk inputlog off"); + gln_normal_string(" to turn off input logs. You can play back" + " recorded commands into a game with the "); + gln_standout_string("glk readlog"); + gln_normal_string(" command.\n"); + } + + else if (matched->handler == gln_command_readlog) { + gln_normal_string("Plays back commands recorded with "); + gln_standout_string("glk inputlog on"); + gln_normal_string(".\n\nUse "); + gln_standout_string("glk readlog on"); + gln_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 == gln_command_abbreviations) { + gln_normal_string("Controls abbreviation expansion.\n\nGlk Level 9" + " automatically expands several standard single" + " letter abbreviations for you; for example, \"x\"" + " becomes \"examine\". Use "); + gln_standout_string("glk abbreviations on"); + gln_normal_string(" to turn this feature on, and "); + gln_standout_string("glk abbreviations off"); + gln_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 == gln_command_graphics) { + gln_normal_string("Turns interpreter graphics on and off.\n\nUse "); + gln_standout_string("glk graphics on"); + gln_normal_string(" to enable interpreter graphics, and "); + gln_standout_string("glk graphics off"); + gln_normal_string(" to turn graphics off and close the graphics window." + " This control works slightly differently to the" + " 'graphics' command in Level 9 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 == gln_command_loopchecks) { + gln_normal_string("Controls game infinite loop monitoring.\n\n" + "Some Level 9 games can enter an infinite loop if they" + " have nothing better to do. A game might do this" + " after it has ended, should you decline its offer" + " to rerun. To avoid the need to kill the interpreter" + " completely if a game does this, Glk Level 9 monitors" + " a game's input and output, and offers the option to" + " end the program gracefully if a game is silent for" + " a few seconds. Use "); + gln_standout_string("glk loopchecks on"); + gln_normal_string(" to turn this feature on, and "); + gln_standout_string("glk loopchecks off"); + gln_normal_string(" to turn it off.\n"); + } + + else if (matched->handler == gln_command_locals) { + gln_normal_string("Controls interception of selected game commands.\n\n" + "Some Level 9 games were written for cassette tape" + " based microprocessor systems, and the way in which" + " they save, restore, and restart games can reflect" + " this. There is also often no straightforward way" + " to quit from a game.\n\nTo make playing a Level 9" + " game appear similar to other systems, Glk Level 9" + " will trap the commands 'quit', 'restart', 'save'," + " 'restore', and 'load' (a synonym for 'restore') and" + " handle them locally within the interpreter. Use "); + gln_standout_string("glk locals on"); + gln_normal_string(" to turn this feature on, and "); + gln_standout_string("glk locals off"); + gln_normal_string(" to turn it off.\n"); + } + + else if (matched->handler == gln_command_prompts) { + gln_normal_string("Controls extra input prompting.\n\n" + "Glk Level 9 can issue a replacement '>' input" + " prompt if it detects that the game hasn't prompted" + " after, say, an empty input line. Use "); + gln_standout_string("glk prompts on"); + gln_normal_string(" to turn this feature on, and "); + gln_standout_string("glk prompts off"); + gln_normal_string(" to turn it off.\n"); + } + + else if (matched->handler == gln_command_version) { + gln_normal_string("Prints the version numbers of the Glk library" + " and the Glk Level 9 port.\n"); + } + + else if (matched->handler == gln_command_commands) { + gln_normal_string("Turn off Glk commands.\n\nUse "); + gln_standout_string("glk commands off"); + gln_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 == gln_command_help) + gln_command_help(""); + + else + gln_normal_string("There is no help available on that Glk command." + " Sorry.\n"); +} + + +/* + * gln_command_escape() + * + * This function is handed each input line. If the line contains a specific + * Glk port command, handle it and return TRUE, otherwise return FALSE. + */ +static int gln_command_escape(const char *string) { + int posn; + char *string_copy, *command, *argument; + assert(string); + + /* + * Return FALSE if the string doesn't begin with the Glk command escape + * introducer. + */ + posn = strspn(string, "\t "); + if (gln_strncasecmp(string + posn, "glk", strlen("glk")) != 0) + return FALSE; + + /* Take a copy of the string, without any leading space or introducer. */ + string_copy = (char *)gln_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) { + gln_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 = GLN_COMMAND_TABLE; entry->command; entry++) { + if (gln_strncasecmp(command, entry->command, strlen(command)) == 0) { + matches++; + matched = entry; + } + } + + /* If the match was unambiguous, call the command handler. */ + if (matches == 1) { + gln_normal_char('\n'); + matched->handler(argument); + + if (!matched->takes_argument && strlen(argument) > 0) { + gln_normal_string("[The "); + gln_standout_string(matched->command); + gln_normal_string(" command ignores arguments.]\n"); + } + } + + /* No match, or the command was ambiguous. */ + else { + gln_normal_string("\nThe Glk command "); + gln_standout_string(command); + gln_normal_string(" is "); + gln_normal_string(matches == 0 ? "not valid" : "ambiguous"); + gln_normal_string(". Try "); + gln_standout_string("glk help"); + gln_normal_string(" for more information.\n"); + } + } else { + gln_normal_char('\n'); + gln_command_help(""); + } + + /* The string contained a Glk command; return TRUE. */ + free(string_copy); + return TRUE; +} + + +/* + * gln_command_intercept() + * + * The Level 9 games handle the commands "quit" and "restart" oddly, and + * somewhat similarly. Both prompt "Press SPACE to play again", and then + * ignore all characters except space. This makes it especially hard to exit + * from a game without killing the interpreter process. They also handle + * "restore" via an odd security mechanism which has no real place here (the + * base Level 9 interpreter sidesteps this with its "#restore" command, and + * has some bugs in "save"). + * + * To try to improve these, here we'll catch and special case the input lines + * "quit", "save", "restore", and "restart". "Load" is a synonym for + * "restore". + * + * On "quit" or "restart", the function sets the interpreter stop reason + * code, stops the current game run. On "save" or "restore" it calls the + * appropriate internal interpreter function. + * + * The return value is TRUE if an intercepted command was found, otherwise + * FALSE. + */ +static int gln_command_intercept(char *string) { + int posn, result; + char *string_copy, *trailing; + assert(string); + + result = FALSE; + + /* Take a copy of the string, excluding any leading whitespace. */ + posn = strspn(string, "\t "); + string_copy = (char *)gln_malloc(strlen(string + posn) + 1); + strcpy(string_copy, string + posn); + + /* + * Find the space or NUL after the first word, and check that anything + * after it the first word is whitespace only. + */ + posn = strcspn(string_copy, "\t "); + trailing = string_copy + posn; + if (trailing[strspn(trailing, "\t ")] == '\0') { + /* Terminate the string copy for easy comparisons. */ + string_copy[posn] = '\0'; + + /* If this command was "quit", confirm, then call StopGame(). */ + if (gln_strcasecmp(string_copy, "quit") == 0) { + if (gln_confirm("\nDo you really want to stop? [Y or N] ")) { + gln_stop_reason = STOP_EXIT; + StopGame(); + } + result = TRUE; + } + + /* If this command was "restart", confirm, then call StopGame(). */ + else if (gln_strcasecmp(string_copy, "restart") == 0) { + if (gln_confirm("\nDo you really want to restart? [Y or N] ")) { + gln_stop_reason = STOP_RESTART; + StopGame(); + } + result = TRUE; + } + + /* If this command was "save", simply call save(). */ + else if (gln_strcasecmp(string_copy, "save") == 0) { + gln_standout_string("\nSaving using interpreter\n\n"); + save(); + result = TRUE; + } + + /* If this command was "restore" or "load", call restore(). */ + else if (gln_strcasecmp(string_copy, "restore") == 0 + || gln_strcasecmp(string_copy, "load") == 0) { + gln_standout_string("\nRestoring using interpreter\n\n"); + restore(); + result = TRUE; + } + } + + free(string_copy); + return result; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port input functions */ +/*---------------------------------------------------------------------*/ + +/* Ctrl-C and Ctrl-U character constants. */ +static const char GLN_CONTROL_C = '\003', + GLN_CONTROL_U = '\025'; + +/* + * os_readchar() call count limit, after which we really read a character. + * Also, call count limit on os_stoplist calls, after which we poll for a + * character press to stop the listing, and a stoplist poll timeout. + */ +static const int GLN_READCHAR_LIMIT = 1024, + GLN_STOPLIST_LIMIT = 10; +static const glui32 GLN_STOPLIST_TIMEOUT = 50; + +/* Quote used to suppress abbreviation expansion and local commands. */ +static const char GLN_QUOTED_INPUT = '\''; + + +/* + * Note of when the interpreter is in list output. The last element of any + * list generally lacks a terminating newline, and unless we do something + * special with it, it'll look like a valid prompt to us. + */ +static int gln_inside_list = FALSE; + + +/* Table of single-character command abbreviations. */ +typedef const struct { + const char abbreviation; /* Abbreviation character. */ + const char *const expansion; /* Expansion string. */ +} gln_abbreviation_t; +typedef gln_abbreviation_t *gln_abbreviationref_t; + +static gln_abbreviation_t GLN_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} +}; + + +/* + * gln_expand_abbreviations() + * + * Expand a few common one-character abbreviations commonly found in other + * game systems, but not always normal in Level 9 games. + */ +static void gln_expand_abbreviations(char *buffer, int size) { + char *command, abbreviation; + const char *expansion; + gln_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 = GLN_ABBREVIATIONS; entry->expansion; entry++) { + if (entry->abbreviation == abbreviation) { + expansion = entry->expansion; + break; + } + } + + /* + * If a match found, check for a fit, then replace the character with the + * expansion string. + */ + if (expansion) { + if (strlen(buffer) + strlen(expansion) - 1 >= (uint)size) + return; + + memmove(command + strlen(expansion) - 1, command, strlen(command) + 1); + memcpy(command, expansion, strlen(expansion)); + +#ifndef GARGLK + gln_standout_string("["); + gln_standout_char(abbreviation); + gln_standout_string(" -> "); + gln_standout_string(expansion); + gln_standout_string("]\n"); +#endif + } +} + + +/* + * gln_output_endlist() + * + * The core interpreter doesn't terminate lists with a newline, so we take + * care of that here; a fixup for input functions. + */ +static void gln_output_endlist(void) { + if (gln_inside_list) { + /* + * Supply the missing newline, using os_printchar() so that list output + * doesn't look like a prompt when we come to flush it. + */ + os_printchar('\n'); + + gln_inside_list = FALSE; + } +} + + +/* + * os_input() + * + * Read a line from the keyboard. This function makes a special case of + * some command strings, and will also perform abbreviation expansion. + */ +gln_bool os_input(char *buffer, int size) { + event_t event; + assert(buffer); + + /* If doing linemode graphics, run all graphic opcodes available. */ + gln_linegraphics_process(); + + /* + * Update the current status line display, flush any pending buffered + * output, and terminate any open list. + */ + gln_status_notify(); + gln_output_endlist(); + gln_output_flush(); + + /* + * Level 9 games tend not to issue a prompt after reading an empty + * line of input, and the Adrian Mole games don't issue a prompt at + * all when outside the 1/2/3 menuing system. 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 (gln_prompt_enabled && !gln_game_prompted()) { + gln_normal_char('\n'); + gln_normal_string(GLN_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 (gln_readlog_stream) { + glui32 chars; + + /* Get the next line from the log stream. */ + chars = g_vm->glk_get_line_stream(gln_readlog_stream, buffer, size); + if (chars > 0) { + /* Echo the line just read in input style. */ + g_vm->glk_set_style(style_Input); + g_vm->glk_put_buffer(buffer, chars); + g_vm->glk_set_style(style_Normal); + + /* Tick the watchdog, and return. */ + gln_watchdog_tick(); + return TRUE; + } + + /* + * We're at the end of the log stream. Close it, and then continue + * on to request a line from Glk. + */ + g_vm->glk_stream_close(gln_readlog_stream, NULL); + gln_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(gln_main_window, buffer, size - 1, 0); + gln_event_wait(evtype_LineInput, &event); + + /* Terminate the input line with a NUL. */ + assert((int)event.val1 <= size - 1); + buffer[event.val1] = '\0'; + + /* + * If neither abbreviations nor local commands are enabled, nor game + * command interceptions, use the data read above without further massaging. + */ + if (gln_abbreviations_enabled + || gln_commands_enabled || gln_intercept_enabled) { + char *command; + + /* + * If the first non-space input character is a quote, bypass all + * abbreviation expansion and local command recognition, and use the + * unadulterated input, less introductory quote. + */ + command = buffer + strspn(buffer, "\t "); + if (command[0] == GLN_QUOTED_INPUT) { + /* Delete the quote with memmove(). */ + memmove(command, command + 1, strlen(command)); + } else { + /* Check for, and expand, and abbreviated commands. */ + if (gln_abbreviations_enabled) + gln_expand_abbreviations(buffer, size); + + /* + * Check for standalone "help", then for Glk port special commands; + * suppress the interpreter's use of this input for Glk commands by + * returning FALSE. + */ + if (gln_commands_enabled) { + int posn; + + posn = strspn(buffer, "\t "); + if (gln_strncasecmp(buffer + posn, "help", strlen("help")) == 0) { + if (strspn(buffer + posn + strlen("help"), "\t ") + == strlen(buffer + posn + strlen("help"))) { + gln_output_register_help_request(); + } + } + + if (gln_command_escape(buffer)) { + gln_output_silence_help_hints(); + gln_watchdog_tick(); + return FALSE; + } + } + + /* + * Check for locally intercepted commands, again returning FALSE if + * one is handled. + */ + if (gln_intercept_enabled) { + if (gln_command_intercept(buffer)) { + gln_watchdog_tick(); + return FALSE; + } + } + } + } + + /* + * If there is an input log active, log this input string to it. Note that + * by logging here we get any abbreviation expansions but we won't log glk + * special commands, nor any input read from a current open input log. + */ + if (gln_inputlog_stream) { + g_vm->glk_put_string_stream(gln_inputlog_stream, buffer); + g_vm->glk_put_char_stream(gln_inputlog_stream, '\n'); + } + + gln_watchdog_tick(); + return TRUE; +} + + +/* + * os_readchar() + * + * Poll the keyboard for characters, and return the character code of any key + * pressed, or 0 if none pressed. + * + * Simple though this sounds, it's tough to do right in a timesharing OS, and + * requires something close to an abuse of Glk. + * + * The initial, tempting, implementation is to wait inside this function for + * a key press, then return the code. Unfortunately, this causes problems in + * the Level 9 interpreter. Here's why: the interpreter is a VM emulating a + * single-user microprocessor system. On such a system, it's quite okay for + * code to spin in a loop waiting for a keypress; there's nothing else + * happening on the system, so it can burn CPU. To wait for a keypress, game + * code might first wait for no-keypress (0 from this function), then a + * keypress (non-0), then no-keypress again (and it does indeed seem to do + * just this). If, in os_readchar(), we simply wait for and return key codes, + * we'll never return a 0, so the above wait for a keypress in the game will + * hang forever. + * + * To make matters more complex, some Level 9 games poll for keypresses as a + * way for a user to halt scrolling. For these polls, we really want to + * return 0, otherwise the output grinds to a halt. Moreover, some games even + * use key polling as a crude form of timeout - poll and increment a counter, + * and exit when either os_readchar() returns non-0, or after some 300 or so + * polls. + * + * So, this function MUST return 0 sometimes, and real key codes other times. + * The solution adopted is best described as expedient. Depending on what Glk + * provides in the way of timers, we'll do one of two things: + * + * o If we have timers, we'll set up a timeout, and poll for a key press + * within that timeout. As a way to smooth output for games that use key + * press polling for scroll control, we'll ignore calls until we get two + * in a row without intervening character output. + * + * o If we don't have timers, then we'll return 0 most of the time, and then + * really wait for a key one time out of some number. A game polling for + * keypresses to halt scrolling will probably be to the point where it + * cannot continue without user input at this juncture, and once we've + * rejected a few hundred calls we can now really wait for Glk key press + * event, and avoid a spinning loop. A game using key polling as crude + * timing may, or may not, time out in the calls for which we return 0. + * + * Empirically, this all seems to work. The only odd behaviour is with the + * DEMO mode of Adrian Mole where Glk has no timers, and this is primarily + * because the DEMO mode relies on the delay of keyboard polling for part of + * its effect; on a modern system, the time to call through here is nowhere + * near the time consumed by the original platform. The other point of note + * is that this all means that we can't return characters from any readlog + * with this function; its timing stuff and its general polling nature make + * it impossible to connect to readlog, so it just won't work at all with the + * Adrian Mole games, Glk timers or otherwise. + */ +char os_readchar(int millis) { + static int call_count = 0; + + event_t event; + char character; + + /* If doing linemode graphics, run all graphic opcodes available. */ + gln_linegraphics_process(); + + /* + * Here's the way we try to emulate keyboard polling for the case of no Glk + * timers. We'll say nothing is pressed for some number of consecutive + * calls, then continue after that number of calls. + */ + if (!g_vm->glk_gestalt(gestalt_Timer, 0)) { + if (++call_count < GLN_READCHAR_LIMIT) { + /* Call tick as we may be outside an opcode loop. */ + g_vm->glk_tick(); + gln_watchdog_tick(); + return 0; + } else + call_count = 0; + } + + /* + * If we have Glk timers, we can smooth game output with games that contin- + * uously use this input function by pretending that there is no keypress + * if the game printed output since the last call. This helps with the + * Adrian Mole games, which check for a keypress at the end of a line as a + * way to temporarily halt scrolling. + */ + if (g_vm->glk_gestalt(gestalt_Timer, 0)) { + if (gln_recent_output()) { + /* Call tick as we may be outside an opcode loop. */ + g_vm->glk_tick(); + gln_watchdog_tick(); + return 0; + } + } + + /* + * Now flush any pending buffered output. We do it here rather than earlier + * as it only needs to be done when we're going to request Glk input, and + * we may have avoided this with the checks above. + */ + gln_status_notify(); + gln_output_endlist(); + gln_output_flush(); + + /* + * Set up a character event request, and a timeout if the Glk library can + * do them, and wait until one or the other occurs. Loop until we read an + * acceptable ASCII character (if we don't time out). + */ + do { + g_vm->glk_request_char_event(gln_main_window); + if (g_vm->glk_gestalt(gestalt_Timer, 0)) { + gln_arbitrate_request_timer_events(millis); + gln_event_wait_2(evtype_CharInput, evtype_Timer, &event); + gln_arbitrate_request_timer_events(0); + + /* + * If the event was a timeout, cancel the unfilled character + * request, and return no-keypress value. + */ + if (event.type == evtype_Timer) { + g_vm->glk_cancel_char_event(gln_main_window); + gln_watchdog_tick(); + return 0; + } + } else + gln_event_wait(evtype_CharInput, &event); + } while (event.val1 > BYTE_MAX && event.val1 != keycode_Return); + + /* Extract the character from the event, converting Return, no echo. */ + character = event.val1 == keycode_Return ? '\n' : event.val1; + + /* + * Special case ^U as a way to press a key on a wait, yet return a code to + * the interpreter as if no key was pressed. Useful if scrolling stops + * where there are no Glk timers, to get scrolling started again. ^U is + * always active. + */ + if (character == GLN_CONTROL_U) { + gln_watchdog_tick(); + return 0; + } + + /* + * Special case ^C to quit the program. Without this, there's no easy way + * to exit from a game that never uses os_input(), but instead continually + * uses just os_readchar(). ^C handling can be disabled with command line + * options. + */ + if (gln_intercept_enabled && character == GLN_CONTROL_C) { + if (gln_confirm("\n\nDo you really want to stop? [Y or N] ")) { + gln_stop_reason = STOP_EXIT; + StopGame(); + + gln_watchdog_tick(); + return 0; + } + } + + /* + * If there is a transcript stream, send the input to it as a single line + * string, otherwise it won't be visible in the transcript. + */ + if (gln_transcript_stream) { + g_vm->glk_put_char_stream(gln_transcript_stream, character); + g_vm->glk_put_char_stream(gln_transcript_stream, '\n'); + } + + /* Finally, return the single character read. */ + gln_watchdog_tick(); + return character; +} + + +/* + * os_stoplist() + * + * This is called from #dictionary listings to poll for a request to stop + * the listing. A check for keypress is usual at this point. However, Glk + * cannot check for keypresses without a delay, which slows listing consid- + * erably, since it also adjusts and renders the display. As a compromise, + * then, we'll check for keypresses on a small percentage of calls, say one + * in ten, which means that listings happen with only a short delay, but + * there's still an opportunity to stop them. + * + * This applies only where the Glk library has timers. Where it doesn't, we + * can't check for keypresses without blocking, so we do no checks at all, + * and let lists always run to completion. + */ +gln_bool os_stoplist(void) { + static int call_count = 0; + + event_t event; + int is_stop_confirmed; + + /* Note that the interpreter is producing a list. */ + gln_inside_list = TRUE; + + /* + * If there are no Glk timers, then polling for a keypress but continuing + * on if there isn't one is not an option. So flush output, return FALSE, + * and just keep listing on to the end. + */ + if (!g_vm->glk_gestalt(gestalt_Timer, 0)) { + gln_output_flush(); + gln_watchdog_tick(); + return FALSE; + } + + /* Increment the call count, and return FALSE if under the limit. */ + if (++call_count < GLN_STOPLIST_LIMIT) { + /* Call tick as we may be outside an opcode loop. */ + g_vm->glk_tick(); + gln_watchdog_tick(); + return FALSE; + } else + call_count = 0; + + /* Flush any pending buffered output, delayed to here in case avoidable. */ + gln_output_flush(); + + /* + * Look for a keypress, with a very short timeout in place, in a similar + * way as done for os_readchar() above. + */ + g_vm->glk_request_char_event(gln_main_window); + gln_arbitrate_request_timer_events(GLN_STOPLIST_TIMEOUT); + gln_event_wait_2(evtype_CharInput, evtype_Timer, &event); + gln_arbitrate_request_timer_events(0); + + /* + * If the event was a timeout, cancel the unfilled character request, and + * return FALSE to continue listing. + */ + if (event.type == evtype_Timer) { + g_vm->glk_cancel_char_event(gln_main_window); + gln_watchdog_tick(); + return FALSE; + } + + /* Keypress detected, so offer to stop listing. */ + assert(event.type == evtype_CharInput); + is_stop_confirmed = gln_confirm("\n\nStop listing? [Y or N] "); + + /* + * As we've output a newline, we no longer consider that we're inside a + * list. Clear the flag, and also clear prompt detection by polling it. + */ + gln_inside_list = FALSE; + gln_game_prompted(); + + /* Return TRUE if stop was confirmed, FALSE to keep listing. */ + gln_watchdog_tick(); + return is_stop_confirmed; +} + + +/* + * gln_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 gln_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. + */ + gln_standout_string(prompt); + + /* Wait for a single 'Y' or 'N' character response. */ + response = ' '; + do { + g_vm->glk_request_char_event(gln_main_window); + gln_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'; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port event functions */ +/*---------------------------------------------------------------------*/ + +/* + * gln_event_wait_2() + * gln_event_wait() + * + * Process Glk events until one of the expected type, or types, arrives. + * Return the event of that type. + */ +static void gln_event_wait_2(glui32 wait_type_1, glui32 wait_type_2, event_t *event) { + assert(event); + + do { + g_vm->glk_select(event); + + switch (event->type) { + case evtype_Arrange: + case evtype_Redraw: + /* Refresh any sensitive windows on size events. */ + gln_status_redraw(); + gln_graphics_paint(); + break; + + case evtype_Timer: + /* Do background graphics updates on timeout. */ + gln_graphics_timeout(); + break; + + default: + break; + } + } while (event->type != (EvType)wait_type_1 && event->type != (EvType)wait_type_2); +} + +static void gln_event_wait(glui32 wait_type, event_t *event) { + assert(event); + gln_event_wait_2(wait_type, evtype_None, event); +} + + +/*---------------------------------------------------------------------*/ +/* Glk port file functions */ +/*---------------------------------------------------------------------*/ + +/* + * os_save_file () + * os_load_file () + * + * Save the current game state to a file, and load a game state. + */ +gln_bool os_save_file(gln_byte *ptr, int bytes) { + frefid_t fileref; + strid_t stream; + assert(ptr); + + /* Flush any pending buffered output. */ + gln_output_flush(); + + fileref = g_vm->glk_fileref_create_by_prompt(fileusage_SavedGame, + filemode_Write, 0); + if (!fileref) { + gln_watchdog_tick(); + return FALSE; + } + + stream = g_vm->glk_stream_open_file(fileref, filemode_Write, 0); + if (!stream) { + g_vm->glk_fileref_destroy(fileref); + gln_watchdog_tick(); + return FALSE; + } + + /* Write game state. */ + g_vm->glk_put_buffer_stream(stream, (const char *)ptr, bytes); + + g_vm->glk_stream_close(stream, NULL); + g_vm->glk_fileref_destroy(fileref); + + gln_watchdog_tick(); + return TRUE; +} + +gln_bool os_load_file(gln_byte *ptr, int *bytes, int max) { + frefid_t fileref; + strid_t stream; + assert(ptr && bytes); + + /* Flush any pending buffered output. */ + gln_output_flush(); + + fileref = g_vm->glk_fileref_create_by_prompt(fileusage_SavedGame, + filemode_Read, 0); + if (!fileref) { + gln_watchdog_tick(); + return FALSE; + } + + /* + * Reject the file reference if we're expecting to read from it, and the + * referenced file doesn't exist. + */ + if (!g_vm->glk_fileref_does_file_exist(fileref)) { + g_vm->glk_fileref_destroy(fileref); + gln_watchdog_tick(); + return FALSE; + } + + stream = g_vm->glk_stream_open_file(fileref, filemode_Read, 0); + if (!stream) { + g_vm->glk_fileref_destroy(fileref); + gln_watchdog_tick(); + return FALSE; + } + + /* Restore saved game data. */ + *bytes = g_vm->glk_get_buffer_stream(stream, (char *)ptr, max); + + g_vm->glk_stream_close(stream, NULL); + g_vm->glk_fileref_destroy(fileref); + + gln_watchdog_tick(); + return TRUE; +} + + +/*---------------------------------------------------------------------*/ +/* Glk port multi-file game functions */ +/*---------------------------------------------------------------------*/ + +/* + * os_get_game_file () + * + * This function is a bit of a cheat. It's called when the emulator has + * detected a request from the game to restart the tape, on a tape-based + * game. Ordinarily, we should prompt the player for the name of the + * system file containing the next game part. Unfortunately, Glk doesn't + * make this at all easy. The requirement is to return a filename, but Glk + * hides these inside fileref_t's, and won't let them out. + * + * Theoretically, according to the porting guide, this function should + * prompt the user for a new game file name, that being the next part of the + * game just (presumably) completed. + * + * However, the newname passed in is always the current game file name, as + * level9.c ensures this for us. If we search for, and find, and then inc- + * rement, the last digit in the filename passed in, we wind up with, in + * all likelihood, the right file path. This is icky. + * + * This function is likely to be a source of portability problems on + * platforms that don't implement a file path/name mechanism that matches + * the expectations of the Level 9 base interpreter fairly closely. + */ +gln_bool os_get_game_file(char *newname, int size) { + char *basename; + int index, digit, file_number; + Common::File f; + assert(newname); + + /* Find the last element of the filename passed in. */ + basename = strrchr(newname, GLN_FILE_DELIM); + basename = basename ? basename + 1 : newname; + + /* Search for the last numeric character in the basename. */ + digit = -1; + for (index = strlen(basename) - 1; index >= 0; index--) { + if (Common::isDigit(basename[index])) { + digit = index; + break; + } + } + if (digit == -1) { + gln_watchdog_tick(); + return FALSE; + } + + /* + * Convert the digit to a file number and increment it. Fail if the new + * file number is outside 1..9. + */ + file_number = basename[digit] - '0' + 1; + if (file_number < 1 || file_number > 9) { + gln_watchdog_tick(); + return FALSE; + } + + /* Write the new number back into the file. */ + basename[digit] = file_number + '0'; + + /* Flush pending output, then display the filename generated. */ + gln_output_flush(); + gln_game_prompted(); + gln_standout_string("\nNext load file: "); + gln_standout_string(basename); + gln_standout_string("\n\n"); + + /* + * Try to confirm access to the file. Otherwise, if we return TRUE but the + * interpreter can't open the file, it stops the game, and we then lose any + * chance to save it before quitting. + */ + if (!Common::File::exists(newname)) { + /* Restore newname to how it was, and return fail. */ + basename[digit] = file_number - 1 + '0'; + gln_watchdog_tick(); + return FALSE; + } + + /* Encourage game name re-lookup, and return success. */ + gln_gameid_game_name_reset(); + gln_watchdog_tick(); + return TRUE; +} + + +/* + * os_set_filenumber() + * + * This function returns the next file in a game series for a disk-based + * game (typically, gamedat1.dat, gamedat2.dat...). It finds a single digit + * in a filename, and resets it to the new value passed in. The implemen- + * tation here is based on the generic interface version, and with the same + * limitations, specifically being limited to file numbers in the range 0 + * to 9, since it works on only the last digit character in the filename + * buffer passed in. + * + * This function may also be a source of portability problems on platforms + * that don't use "traditional" file path schemes. + */ +void os_set_filenumber(char *newname, int size, int file_number) { + char *basename; + int index, digit; + assert(newname); + + /* Do nothing if the file number is beyond what we can handle. */ + if (file_number < 0 || file_number > 9) { + gln_watchdog_tick(); + return; + } + + /* Find the last element of the new filename. */ + basename = strrchr(newname, GLN_FILE_DELIM); + basename = basename ? basename + 1 : newname; + + /* Search for the last numeric character in the basename. */ + digit = -1; + for (index = strlen(basename) - 1; index >= 0; index--) { + if (Common::isDigit(basename[index])) { + digit = index; + break; + } + } + if (digit == -1) { + gln_watchdog_tick(); + return; + } + + /* Reset the digit in the file name. */ + basename[digit] = file_number + '0'; + + /* Flush pending output, then display the filename generated. */ + gln_output_flush(); + gln_game_prompted(); + gln_standout_string("\nNext disk file: "); + gln_standout_string(basename); + gln_standout_string("\n\n"); + + /* Encourage game name re-lookup, and return. */ + gln_gameid_game_name_reset(); + gln_watchdog_tick(); +} + + +/* + * os_open_script_file() + * + * Handles player calls to the "#play" meta-command. Because we have our + * own way of handling scripts, this function is a stub. + */ +Common::SeekableReadStream *os_open_script_file() { + return NULL; +} + + +/*---------------------------------------------------------------------*/ +/* 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 */ +/*---------------------------------------------------------------------*/ + +/* + * Watchdog timeout -- we'll wait for five seconds of silence from the core + * interpreter before offering to stop the game forcibly, and we'll check + * it every 10,240 opcodes. + */ +static const int GLN_WATCHDOG_TIMEOUT = 5, + GLN_WATCHDOG_PERIOD = 10240; + +/* + * The following values need to be passed between the startup_code and main + * functions. + */ +static const char *gln_gamefile = NULL, /* Name of game file. */ + *gln_game_message = NULL; /* Error message. */ + + +/* + * gln_establish_picture_filename() + * + * Given a game name, try to create an (optional) graphics data file. For + * an input "file" X, the function looks for X.PIC or X.pic, then for + * PICTURE.DAT or picture.dat in the same directory as X. If the input file + * already ends with a three-letter extension, it's stripped first. + * + * The function returns NULL if a graphics file is not available. It's not + * fatal for this to be the case. Filenames are malloc'ed, and need to be + * freed by the caller. + * + * The function uses fopen() rather than access() since fopen() is an ANSI + * standard function, and access() isn't. + */ +static void gln_establish_picture_filename(const char *name, char **graphics) { + char *base, *directory_end, *graphics_file; + Common::File f; + assert(name && graphics); + + /* Take a destroyable copy of the input filename. */ + base = (char *)gln_malloc(strlen(name) + 1); + strcpy(base, name); + + /* If base has an extension .LEV, .SNA, or similar, remove it. */ + if (strrchr(base, '.')) { + base[strlen(base) - strlen(strrchr(base, '.'))] = '\0'; + } + + /* Allocate space for the return graphics file. */ + graphics_file = (char *)gln_malloc(strlen(base) + strlen(".___") + 1); + + /* Form a candidate graphics file, using a .PIC extension. */ + if (f.isOpen()) { + strcpy(graphics_file, base); + strcat(graphics_file, ".PIC"); + f.open(graphics_file); + } + + if (f.isOpen()) { + strcpy(graphics_file, base); + strcat(graphics_file, ".pic"); + f.open(graphics_file); + } + + /* Form a candidate graphics file, using a .CGA extension. */ + if (f.isOpen()) { + strcpy(graphics_file, base); + strcat(graphics_file, ".CGA"); + f.open(graphics_file); + } + + if (f.isOpen()) { + strcpy(graphics_file, base); + strcat(graphics_file, ".cga"); + f.open(graphics_file); + } + + /* Form a candidate graphics file, using a .HRC extension. */ + if (f.isOpen()) { + strcpy(graphics_file, base); + strcat(graphics_file, ".HRC"); + f.open(graphics_file); + } + + if (f.isOpen()) { + strcpy(graphics_file, base); + strcat(graphics_file, ".hrc"); + f.open(graphics_file); + } + + /* No access to graphics file. */ + if (f.isOpen()) { + free(graphics_file); + graphics_file = NULL; + } + + f.close(); + + /* If we found a graphics file, return its name immediately. */ + if (graphics_file) { + *graphics = graphics_file; + free(base); + return; + } + + /* Retry with base set to the game file directory part only. */ + directory_end = strrchr(base, GLN_FILE_DELIM); + directory_end = directory_end ? directory_end + 1 : base; + base[directory_end - base] = '\0'; + + /* Again, allocate space for the return graphics file. */ + graphics_file = (char *)gln_malloc(strlen(base) + strlen("PICTURE.DAT") + 1); + + /* As above, form a candidate graphics file. */ + strcpy(graphics_file, base); + strcat(graphics_file, "PICTURE.DAT"); + + if (!f.open(graphics_file)) { + /* Retry, using picture.dat extension instead. */ + strcpy(graphics_file, base); + strcat(graphics_file, "picture.dat"); + if (!f.open(graphics_file)) { + /* + * No access to this graphics file. In this case, free memory + * and reset graphics file to NULL. + */ + free(graphics_file); + graphics_file = NULL; + } + } + + f.close(); + + /* + * Return whatever we found for the graphics file (NULL if none found), + * and free base. + */ + *graphics = graphics_file; + free(base); +} + + +/* + * gln_startup_code() + * gln_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. + */ +static int gln_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], "-ni") == 0) { + gln_intercept_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-nc") == 0) { + gln_commands_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-na") == 0) { + gln_abbreviations_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-np") == 0) { + gln_graphics_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-ne") == 0) { + gln_prompt_enabled = FALSE; + continue; + } + if (strcmp(argv[argv_index], "-nl") == 0) { + gln_loopcheck_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 pointerto NULL, and + * complain about it later in main. Passing the message string around + * like this is a nuisance... + */ + if (argv_index == argc - 1) { + gln_gamefile = argv[argv_index]; + gln_game_message = NULL; +#ifdef GARGLK + { + const char *s; + s = strrchr(gln_gamefile, '\\'); + if (s) + g_vm->garglk_set_story_name(s + 1); + s = strrchr(gln_gamefile, '/'); + if (s) + g_vm->garglk_set_story_name(s + 1); + } +#endif + } else { + gln_gamefile = NULL; + if (argv_index < argc - 1) + gln_game_message = "More than one game file was given" + " on the command line."; + else + gln_game_message = "No game file was given on the command line."; + } + + /* All startup options were handled successfully. */ + return TRUE; +} + +static void gln_main(void) { + char *graphics_file = NULL; + int is_running; + + /* Ensure Level 9 internal types have the right sizes. */ + if (!(sizeof(gln_byte) == 1 + && sizeof(gln_uint16) == 2 && sizeof(gln_uint32) == 4)) { + gln_fatal("GLK: Types sized incorrectly, recompilation is needed"); + g_vm->glk_exit(); + } + + /* Create the main Glk window, and set its stream as current. */ + gln_main_window = g_vm->glk_window_open(0, 0, 0, wintype_TextBuffer, 0); + if (!gln_main_window) { + gln_fatal("GLK: Can't open main window"); + g_vm->glk_exit(); + } + g_vm->glk_window_clear(gln_main_window); + g_vm->glk_set_window(gln_main_window); + g_vm->glk_set_style(style_Normal); + + /* If there's a problem with the game file, complain now. */ + if (!gln_gamefile) { + assert(gln_game_message); + gln_header_string("Glk Level 9 Error\n\n"); + gln_normal_string(gln_game_message); + gln_normal_char('\n'); + g_vm->glk_exit(); + } + + /* + * Given the basic game name, try to come up with a usable graphics + * filenames. The graphics file may be null. + */ + gln_establish_picture_filename(gln_gamefile, &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. + */ + gln_graphics_possible = g_vm->glk_gestalt(gestalt_Graphics, 0) + && g_vm->glk_gestalt(gestalt_Timer, 0); + + /* + * 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 (!gln_graphics_possible) + gln_graphics_enabled = FALSE; + + /* If pictures are possible, search for bitmap graphics. */ + if (gln_graphics_possible) + gln_graphics_locate_bitmaps(gln_gamefile); + + /* Try to create a one-line status window. We can live without it. */ + /* + gln_status_window = g_vm->glk_window_open (gln_main_window, + winmethod_Above | winmethod_Fixed, + 1, wintype_TextGrid, 0); + */ + + /* Repeat this game until no more restarts requested. */ + do { + g_vm->glk_window_clear(gln_main_window); + + /* + * In a multi-file game, restarting may mean reverting back to part one + * of the game. So we have to encourage a re-lookup of the game name + * at this point. + */ + gln_gameid_game_name_reset(); + + /* Load the game, sending in any established graphics file. */ + int errNum = 0; + if (!LoadGame(gln_gamefile, graphics_file)) { + if (gln_status_window) + g_vm->glk_window_close(gln_status_window, NULL); + gln_header_string("Glk Level 9 Error\n\n"); + gln_normal_string("Can't find, open, or load game file '"); + gln_normal_string(gln_gamefile); + gln_normal_char('\''); + if (errNum != 0) { + gln_normal_string(": ERROR"); + } + gln_normal_char('\n'); + + /* + * Nothing more to be done, so we'll free interpreter allocated + * memory, then break rather than exit, to run memory cleanup and + * close any open streams. + */ + FreeMemory(); + break; + } + + /* Print out a short banner. */ + gln_header_string("\nLevel 9 Interpreter, version 5.1\n"); + gln_banner_string("Written by Glen Summers and David Kinder\n" + "Glk interface by Simon Baldwin\n\n"); + + /* + * Set the stop reason indicator to none. A game will then exit with a + * reason if we call StopGame(), or none if it exits of its own accord + * (or with the "#quit" command, say). + */ + gln_stop_reason = STOP_NONE; + + /* Start, or restart, watchdog checking. */ + gln_watchdog_start(GLN_WATCHDOG_TIMEOUT, GLN_WATCHDOG_PERIOD); + + /* Run the game until StopGame called, or RunGame() returns FALSE. */ + do { + is_running = RunGame(); + g_vm->glk_tick(); + + /* Poll for watchdog timeout. */ + if (is_running && gln_watchdog_has_timed_out()) { + gln_stop_reason = STOP_FORCE; + StopGame(); + break; + } + } while (is_running); + + /* Stop watchdog functions, and flush any pending buffered output. */ + gln_watchdog_stop(); + gln_status_notify(); + gln_output_flush(); + + /* Free interpreter allocated memory. */ + FreeMemory(); + + /* + * Unset any "stuck" game 'cheating' flag. This can get stuck on if + * exit is forced from the #cheat mode in the Adrian Mole games, which + * otherwise loop infinitely. Unsetting the flag here permits restarts; + * without this, the core interpreter remains permanently in silent + * #cheat mode. + */ + Cheating = FALSE; + + /* + * If the stop reason is none, something in the game stopped itself, or + * the user entered "#quit". If the stop reason is force, the user + * terminated because of an apparent infinite loop. For both of these, + * offer the choice to restart, or not (equivalent to exit). + */ + if (gln_stop_reason == STOP_NONE || gln_stop_reason == STOP_FORCE) { + gln_standout_string(gln_stop_reason == STOP_NONE + ? "\nThe game has exited.\n" + : "\nGame exit was forced. The current game" + " state is unrecoverable. Sorry.\n"); + + if (gln_confirm("\nDo you want to restart? [Y or N] ")) + gln_stop_reason = STOP_RESTART; + else + gln_stop_reason = STOP_EXIT; + } + } while (gln_stop_reason == STOP_RESTART); + + /* Free any temporary memory that may have been used by graphics. */ + gln_graphics_cleanup(); + gln_linegraphics_cleanup(); + + /* Close any open transcript, input log, and/or read log. */ + if (gln_transcript_stream) { + g_vm->glk_stream_close(gln_transcript_stream, NULL); + gln_transcript_stream = NULL; + } + if (gln_inputlog_stream) { + g_vm->glk_stream_close(gln_inputlog_stream, NULL); + gln_inputlog_stream = NULL; + } + if (gln_readlog_stream) { + g_vm->glk_stream_close(gln_readlog_stream, NULL); + gln_readlog_stream = NULL; + } + + /* Free any graphics file path. */ + free(graphics_file); +} + + +/*---------------------------------------------------------------------*/ +/* Linkage between Glk entry/exit calls and the real interpreter */ +/*---------------------------------------------------------------------*/ + +/* + * Safety flags, to ensure we always get startup before main, and that + * we only get a call to main once. + */ +static int gln_startup_called = FALSE, + gln_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(gln_startup_called && !gln_main_called); + gln_main_called = TRUE; + + /* Call the interpreter main function. */ + gln_main(); +} + + +/*---------------------------------------------------------------------*/ +/* Glk linkage relevant only to the UNIX platform */ +/*---------------------------------------------------------------------*/ +#ifdef UNUSED + +/* + * Glk arguments for UNIX versions of the Glk interpreter. + */ +glkunix_argumentlist_t glkunix_arguments[] = { + { + (char *) "-nc", glkunix_arg_NoValue, + (char *) "-nc No local handling for Glk special commands" + }, + { + (char *) "-na", glkunix_arg_NoValue, + (char *) "-na Turn off abbreviation expansions" + }, + { + (char *) "-ni", glkunix_arg_NoValue, + (char *) "-ni No local handling for 'quit', 'restart'," + " 'save', and 'restore'" + }, + { + (char *) "-np", glkunix_arg_NoValue, + (char *) "-np Turn off pictures" + }, + { + (char *) "-ne", glkunix_arg_NoValue, + (char *) "-ne Turn off additional interpreter prompt" + }, + { + (char *) "-nl", glkunix_arg_NoValue, + (char *) "-nl Turn off infinite loop detection" + }, + { + (char *) "", glkunix_arg_ValueCanFollow, + (char *) "filename game to run" + }, + {NULL, glkunix_arg_End, NULL} +}; + + +/* + * glkunix_startup_code() + * + * Startup entry point for UNIX versions of Glk interpreter. Glk will + * call glkunix_startup_code() to pass in arguments. On startup, we call + * our function to parse arguments and generally set stuff up. + */ +int glkunix_startup_code(glkunix_startup_t *data) { + assert(!gln_startup_called); + gln_startup_called = TRUE; + +#ifdef GARGLK + garg_vm->glk_set_program_name("Level 9 5.1"); + garg_vm->glk_set_program_info( + "Level 9 5.1 by Glen Summers, David Kinder\n" + "Alan Staniforth, Simon Baldwin and Dieter Baron\n" + "Glk Graphics support by Tor Andersson\n"); +#endif + + return gln_startup_code(data->argc, data->argv); +} +#endif + +} // End of namespace Level9 +} // End of namespace Glk |