Level 9 interpreter Version 5.1 This guide is by David Kinder. Level9 has already been compiled on Windows, MS-DOS, Apple Mac, Amiga, Acorn and Unix systems, so it should be quite easy to get it working on any other modern computer. The first thing that must be done is to check the typedefs in level9.h. The typedefs must have the following properties: L9BYTE unsigned 8 bit quantity L9UINT16 unsigned 16 bit quantity L9UINT32 unsigned 32 bit quantity L9BOOL quantity capable of holding the values TRUE (1) and FALSE (0) Also the define MAX_PATH (the maximum length of the full pathname of a file) must be set, e.g. #define MAX_PATH 256 If graphics are not supported defining NO_SCAN_GRAPHICS will stop the intialization code from looking for graphics data, which may take a noticeable length of time on slower computers. It is required that several os_ functions be written for your system. Given below is a guide to these functions, and a very simple interface is included in generic.c. void os_printchar(char c) os_printchar() prints a character to the output. The interface can either buffer this character or print it immediately, but if buffering is used then the characters must all be sent to the output when the interpreter calls os_flush(). A paragraph of text is output as one long stream of characters, without line breaks, so the interface must provide its own word wrapping and any other features that are desired, such as justification or a [More] prompt. The carriage return character is always '\r', rather than '\n'. L9BOOL os_input(char* ibuff, int size) os_input() reads a line of text from the user, usually to accept the next command to be sent to the game. The text input must be stored in ibuff with a terminating zero, and be no longer than size characters. Normally os_input() should return TRUE, but may return FALSE to cause the entire input so far to be discarded. The reason for doing so is discussed in the section at the end on allowing the interpreter to load a new game without exiting. char os_readchar(L9UINT32 millis) os_readchar() looks to see if a key has been pressed if one has, returns the character to the interpreter immediately. If no key has been pressed the interpreter should wait for a key for at least the number of milliseconds given in the argument. If after this period no key has been pressed, 0 should be returned. This is most commonly used when a game is exited, causing it to print "Press SPACE to play again" and then call os_readchar(). L9BOOL os_stoplist(void) Called during dictionary listing. If true is returned (typically because the user has pressed a key) then the listing is stopped. This routine should return immediately, without waiting. If this is not possible then FALSE should be returned. void os_flush(void) If the calls to os_printchar() are being buffered by the interface then the buffered text must be printed when os_flush() is called. L9BOOL os_save_file(L9BYTE* Ptr, int Bytes) os_save_file() should prompt the user in some way (with either text or a file requester) for a filename to save the area of memory of size Bytes pointed to by Ptr. TRUE or FALSE should be returned depending on whether or not the operation was successful. L9BOOL os_load_file(L9BYTE* Ptr, int* Bytes, int Max) os_load_file() should prompt the user for the name of a file to load. At most Max bytes should be loaded into the memory pointed to by Ptr, and the number of bytes read should be placed into the variable pointed to by Bytes. L9BOOL os_get_game_file(char* NewName, int Size) os_get_game_file() should prompt the user for a new game file, to be stored in NewName, which can take a maximum name of Size characters. When this function is called the NewName array contains the name of the currently loaded game, which can be used to derive a name to prompt the user with. This is used by at least the Adrian Mole games, which load in the next part of the game after the part currently being played has been completed. These games were originally written for tape-based systems where the call was simply "load the next game from the tape". void os_set_filenumber(char* NewName, int Size, int n) os_set_filename() is for multi-part games originally written for disk-based systems, which used game filenames such as gamedat1.dat gamedat2.dat etc. The routine should take the full filename in NewName (of maximum size Size) and modify it to reflect the number n, e.g. os_set_filename("gamedat1.dat",2) should leave "gamedat2.dat" in NewName. FILE* os_open_script_file(void) os_open_script_file() should prompt the user for the name of a script file, from which input will be read until the end of the script file is reached, and should return a pointer to the opened script file, or NULL. This function is called in response to the player entering the "#play" meta-command. void os_graphics(int mode) Called when graphics are turned on or off, either by the game or by the user entering "graphics" or "text" as a command. If mode is 0 graphics should be turned off. If mode is 1 then line drawn graphics will follow, so graphics should be turned on. If mode is 2 then bitmap graphics will follow, so graphics should be turned on, provided that appropriate bitmap graphics files are available (This can be determined by calling DetectBitmaps(), which is discussed below.). After an os_graphics(0) call all the other graphics functions should do nothing. Typically, if mode is not 0 the code will allocate some suitable bitmap for drawing graphics into. For line drawn graphics, to determine the size of the bitmap the code should call GetPictureSize(). The graphics routines should draw in a bitmap of the size returned by this function, and then scale the bitmap appropriately for display. If instead the graphics code tries to scale the co-ordinates passed to os_drawline() and os_fill() then problems occur with fill colours "leaking" into other areas of the picture. The values returned by GetPictureSize() will not change unless a new game is loaded. The graphics bitmap for line drawn graphics is always 4 colour. The 4 colours are chosen from a possible 8 by calls to os_setcolour (see below). Note that a call to os_setcolour() must affect the colour of pixels already drawn on the bitmap. For example, suppose a pixel in the bitmap is set to the first colour in the palette during drawing, which at that moment is red. If later the first colour in the palette is set to blue, at the end the pixel should be shown blue. In order to actually draw graphics, the input routines os_input() and os_readchar() should call RunGraphics(). This is discussed further below. void os_cleargraphics(void) Clear the current graphics bitmap by filling the entire bitmap with colour 0. void os_setcolour(int colour, int index) Set the given colour in the graphics bitmap's palette to the colour at the given index in the interpreter's table of colours. The actual table of colours in the interpreters provided by Level 9 vary across different machines. An acceptable palette that matches reasonably closely to the Amiga releases is as follows (all colours are 8 bit R,G,B): 0x00,0x00,0x00 (black) 0xFF,0x00,0x00 (red) 0x30,0xE8,0x30 (green) 0xFF,0xFF,0x00 (yellow) 0x00,0x00,0xFF (blue) 0xA0,0x68,0x00 (brown) 0x00,0xFF,0xFF (cyan) 0xFF,0xFF,0xFF (white) void os_drawline(int x1, int y1, int x2, int y2, int colour1, int colour2) Draw a line on the graphics bitmap between (x1,y1) and (x2,y2). Note that either point may lie outside of the bitmap, and that it is the responsibility of the routine to clip to the appropriate co-ordinates. For each point on the line, if the colour at that point is equal to colour2 the pixel's colour should be changed to colour1, else it should not be modified. void os_fill(int x, int y, int colour1, int colour2) If the pixel's colour at (x,y) is equal to colour2, fill the region containing (x,y) with colour1. The boundaries of the region are defined as those areas of the bitmap with a colour other than colour2. void os_show_bitmap(int pic, int x, int y) Show the bitmap given by the number pic at the co-ordinates (x,y). Note that the game can request the same picture several times in a row: it is a good idea for ports to record the last picture number and check it against any new requests. The interpreter source code provides a decoder that understands most Level 9 bitmap formats. The decoder is accessed by calling DecodeBitmap(), which is discussed below. int main(int argc, char** argv) You must provide your own main() entry point for the program. The simplest such main() is given in generic.c, which just calls LoadGame() and then sits in a loop calling RunGame(). These functions are discussed below. The interpreter provides several functions to be called by the interface code. These are: L9BOOL LoadGame(char* filename, char* picname) LoadGame() attempts to load filename and then searches it for a valid Level 9 game. If it is successful TRUE is returned, else FALSE. The previous game in memory will be overwritten if the file filename can be loaded, even if it does not contain a Level 9 game, so even if LoadGame() returns FALSE it must be assumed that the game memory has changed. The second argument is the name of the file containing picture data, and may be NULL. Ports should usually ask the user for just the filename and derive picname from it in some way. The recommended approach is to first try the filename with an extension of ".pic" and then try replacing the filename with "picture.dat". L9BOOL RunGame(void) If LoadGame() has been successful, RunGame() can be called to run the Level 9 game. Each call to RunGame() executes a single opcode of the game. In pre-emptive multitasking systems or systems without any multitasking it is enough to sit in a loop calling RunGame(), e.g. while (RunGame()); RunGame() returns TRUE if an opcode code was executed and FALSE if the game is stopped, either by an error or by a call to StopGame(). void StopGame(void) StopGame() stops the current game from playing. void RestoreGame(char *inFile) RestoreGame() attempts to restore the currently running game to the position stored in the inFile saved game file. This gives interface code a means to restore a game position. void FreeMemory(void) FreeMemory() frees memory used to store the game. This routine should be called when exiting the interpreter. void GetPictureSize(int* width, int* height) Returns the width and height of the bitmap that graphics should be drawn into. This is constant for any particular game. L9BOOL RunGraphics(void) Runs an opcode of the graphics routines. If a graphics opcode was run TRUE is returned, otherwise FALSE. The simplest way to get graphics to display is to add a loop to repeatedly call RunGraphics() to os_input() and os_readchar(): while (RunGraphics()); /* Now draw graphics bitmap on display... */ Optionally, the code can provide a more "atmospheric" recreation of the games by drawing the graphics slowly, as was the case on the old 8-bit computers. This is achieved by calling RunGraphics() several times then waiting for a while before calling it again. Note that when waiting the code should still respond to user input. BitmapType DetectBitmaps(char* dir) Given a directory, returns the type of bitmap picture files in it, or NO_BITMAPS if there are no bitmaps. This function is only available if the preprocessor symbol BITMAP_DECODER is defined. Bitmap* DecodeBitmap(char* dir, BitmapType type, int num, int x, int y) This function loads and decodes the specified bitmap, returning a Bitmap structure. The structure contains the width and height of the bitmap, a palette of up to 32 colours used in the bitmap, and the actual data as an array of indexes into the palette. This function is only available if the preprocessor symbol BITMAP_DECODER is defined. One more complex feature of the interpreter is that a new Level 9 game can be loaded without exiting and restarting the interpreter. This is of use in a windowing environment. In this case, both main() and the code that catches a "New Game" menu item should call a routine such as the example new_game() below. This ensures that each new game does not use up more and more of the interpreter's stack. int newgame(char* game_name) { static int playing = FALSE; if (LoadGame(game,NULL)) { if (playing) return -1; playing = TRUE; while (RunGame()); playing = FALSE; } else warn("Unable to load game"); return -1; }