Freescape Analysis

Document last updated on 2024-10-22

Introduction

Acknowledgment: The data on this page owes a lot to the research done by John Elliott and expands on that with results from my own research efforts and focusses more on the PC version than the 8-bit version John has focussed on.

Driller (a.k.a. Space Station Oblivion in the US) was the first of Incentive Software's games using the Freescape system. The data format for the various Freescape games is likely similar for each one and the data format for the 3D Construction Kit (a.k.a. Virtual Reality Studio in the US) 1 and 2 is an evolution of the Freescape format.

Executables

On the PC version of Driller, the main executable, DRILLER.EXE loads customised executables for each graphics mode. DRILLC.EXE for CGA 4 colour mode, DRILLE.EXE for EGA 16 colour mode, DRILLH.EXE for Hercules Monochrome and DRILLT.EXE for Tandy 16 colour mode. Although they are valid DOS MZ executables, they cannot be run directly and are loaded as needed by the main DRILLER.EXE program. Directly following the MZ header block (calculated by taking the number of paragraphs in the header, which is 16-bit word at offset 0x0008, and then multiplying it by the number of bytes in a paragraph, which is 16 - for most MZ executables, including all the Driller ones, this ends up being 512 bytes or offset 0x0200) there is a 16-bit word that specifies the offset from the start of the EXE data to the main game database. In the EGA version, this is 0x9940, meaning the game database starts at file offset 0x9B40. In the CGA version, these are 0x79B0 and 0x7BB0 respectively. When absolute file offsets are mentioned in this document, they always refer to the EGA version.

3D coordinates

The coordinate system of each area covers a half cube with a square base of 127x127 grid units, allowing all coordinates to exist in a 7-bit value, avoiding ambiguity between signed or unsigned coordinates in 8-bit values. The height of the room is 63 grid units. As movement by grid units would be far too coarse, each grid unit is subdivided in 64 subunits, resulting in player coordinates during gameplay ranging from 0..8128 horizontally and from 0..4032 vertically. These finer coordinates are also displayed for the player on the instrument panel. The internal coordinate system uses the X axis running in the eastern direction, the Y axis to run in the up direction and the Z axis running in the northern direction. Thus internally the coordinates on the ground plane are the X, Z coordinates and the Y coordinate is the elevation, the ground plane coordinates are displayed as X,Y to the player and the elevation is labelled with an ↑ arrow.

Performance

The Freescape engine in Driller is capped at a frame rate of 10fps. While it may run slower, it will never run faster. For performance reasons, the game does not run the global and room scripts every 100ms game tick, but only:

  1. On a change in the 3D perspective (which happens only when the camera position or direction changes - not upon shooting, nor when an object is shown or hidden, nor when the REDRAW opcode is issued or even when switching to/from the info screen)
  2. On the tick where the minute counter on the clock changes (i.e. whenever the clock changes to read HH:MM:59).

For player movement, collision detection does not check the actual geometry, Freescape uses only an object's bounding box. When shooting the lasers, however, a pixel accurate check is made against the actual polygons drawn. The difference is noticeable only for pyramids, rectangles and polygons (especially when their data is invalid and they are non-planar).

Bitmapped images

Driller contains two bitmap images. One for the title screen, and another for the background of the cockpit surrounding the 3D view (which is also used as the menu screen). You'd be forgiven for never having seen the title screen, as it was displayed only during loading and unless you were running on an extremely slow system with a similarly slow storage device (e.g. running on an IBM XT from floppy disk) it would flash by in an instant, if it was even visible at all.

The title image is stored in a separate file, SCN1E.DAT for EGA, SCN1C.DAT for 4-colour CGA, and SCN1H.DAT for Hercules Monochrome. The border image is embedded in the overlay executable at offset 0x0210. These bitmaps are 320x200 pixels (CGA and EGA) or 720x348 (Hercules Monochrome) and stored using a form of RLE and the image data (regardless of whether it is) is always preceded by the size of the RLE data in big-endian format.

For Space Station Oblivion, the US version of Driller, the title image is stored in EGATITLE.RL for EGA and CGATITLE.RL for both CGA and Hercules. These are encoded with a similar but different compression method.

Compression scheme (main)

The data is compressed per line from top to bottom. Each line is split up into bitplanes, according to the bit depth of the display. Hercules Monochrome has a single bitplane, CGA 4-colour has 2 bitplanes, and EGA 16-colour has 4 bitplanes. Within each bitplane line, the data is compressed as pairs of bytes. The first byte is a command code, where the upper bit 0x80 specifies a mode, and the lower 7 bits are used to set a repeat count. The second byte in each pair is always raw data for 8 adjacent pixels, with the leftmost pixel stored in the upper bit(s) of the data byte.

Here is pseudocode to decode the image data. const IMAGE_WIDTH = 320; //720 for Hercules const IMAGE_HEIGHT = 200; //348 for Hercules const BIT_PLANES = 4; //1 for Hercules, 2 for CGA for (y = 0; y < IMAGE_HEIGHT; ++y) { for (bp = 0; bp < BIT_PLANES; ++bp) { x = 0; while (x < IMAGE_WIDTH) { command = readbyte(); if (command & 0x80) { repeat_count = 257 - command; data = readbyte(); for (i = 0; i < repeat_count; ++i) { mask = 0x80; for (bit = 0; bit < 8; ++bit) { set_bitplane_pixel_bit(x + bit, y, bp, (data & mask) ? 1 : 0); mask >>= 1; } x += 8; } } else { repeat_count = command + 1; for (i = 0; i < repeat_count; ++i) { data = readbyte(); mask = 0x80; for (bit = 0; bit < 8; ++bit) { set_bitplane_pixel_bit(x + bit, y, bp, (data & mask) ? 1 : 0); mask >>= 1; } x += 8; } } } } }

Compression scheme (alternate)

The title screens for Space Station Oblivion are compressed according to a highly similar scheme. Again, the data is compressed from top to bottom, but there is no splitting into bitplanes. The pixels are just packed from left to right in the byte data, with the leftmost pixel stored in the upper bits of a byte. Unlike the main compression scheme, which operates on a byte of pixel data (2, 4 or 8 pixels) at a time, the alternate compression scheme operates on 16-bit words of pixel data (4 or 8 pixels) at a time. Each line's data starts with a start-of-line marker of 0x02 and a value of 0x00 indicates there are no more lines. Both the values 0x02 and 0x00 are also found in the actual pixel data. Since the number of pixels in a 16-bit word is 4 or 8, both of which evenly divide 320, the encoding scheme allows 320 pixel wide images to always be stored without over- or underrun.

Here is pseudocode to decode the image data. const IMAGE_WIDTH = 320; const IMAGE_HEIGHT = 200; const BIT_PLANES = 4; //2 for CGA (and thus also Hercules) const BITS_PER_WORD = 16; const PIXELS_PER_WORD = BITS_PER_WORD / BIT_PLANES; const PLANE_MASK = (1 << BIT_PLANES) - 1; for (y = 0; y < IMAGE_HEIGHT; ++y) { sol = readbyte(); //Must be 0x02 (0x00 indicates end of file) x = 0; while (x < IMAGE_WIDTH) { command = readbyte(); if (command & 0x80) { repeat_count = 257 - command; for (i = 0; i < repeat_count; ++i) { data = readbyte() << 8; data |= readbyte(); shift = BITS_PER_WORD - BIT_PLANES; mask = PLANE_MASK << shift; for (j = 0; j < PIXELS_PER_WORD; ++j) { pixel = (data & mask) >> shift set_pixel(x + j, y, pixel); mask >>= BIT_PLANES; shift -= BIT_PLANES; } x += PIXELS_PER_WORD; } } else { repeat_count = command + 1; data = readbyte() << 8; data |= readbyte(); for (i = 0; i < repeat_count; ++i) { shift = BITS_PER_WORD - BIT_PLANES; mask = PLANE_MASK << shift; for (j = 0; j < PIXELS_PER_WORD; ++j) { pixel = (data & mask) >> shift set_pixel(x + j, y, pixel); mask >>= BIT_PLANES; shift -= BIT_PLANES; } x += PIXELS_PER_WORD; } } } }

Main database format

Note: 16-bit values are in little endian, as is commonly used on x86 and compatible processors. This is the default for DOS and Windows.

Offset Size in bytes Description
0000 1 Number of rooms in the database
0001 2 Total length of the database (16 bit value)
0003 1 ID of room in which you start the game
0004 1 ID of the entrance in the starting room
0005 1 unknown
0006 1 Initial energy level of the reconnaissance jet
0007 1 Initial shield level of the reconnaissance jet
0008 1 Initial energy level of the excavation probe
0009 1 Initial shield level of the excavation probe
000A 60 Colour palette. Each of the palette entries 1 through 15 (palette entry 0 is unused) consists of four bytes. (see palette below)
0046 2 16-bit offset to the demo data, relative to database start (see demo data below) - in Driller EGA, the absolute file offset is 0x9ED4
0048 2 16-bit offset to the global bytecode routines, relative to database start - in Driller EGA, the absolute file offset is 0x9C4E
004A 106 unknown
00B4 8 HH:MM:SS as a text string specifying the starting value of the timer where HH, MM and SS are the hours, minutes and seconds respectively.
00BC 2 unknown
00BE 1 Initial extend level, 1-based (so a value of 1 means an extend level of 0)
00BF 2 unknown
00C1 1 Start in probe (0) or jet (non-zero)
00C2 1 Initial step index (0-6). Values result in the following step size:
00 - 1
01 - 2
02 - 5
03 - 10
04 - 25
05 - 50
06 - 100
00C3 1 Initial angle (0-5). Values result in the following angle steps:
00 - 5
01 - 10
02 - 15
03 - 30
04 - 45
05 - 90
00C4 4 unknown
00C8 70 A table of 35 values of 16-bit offsets to room definitions, relative to the start of the database. If a table entry is 0000, the entry is unused. Although there may only be a maximum of 35 rooms in the database, their IDs need not match their indices.

Palette

The table below specifies the 16-colour CGA text mode palette, which was also the default palette in the EGA standard and the Tandy/PCjr's graphics cards. The data from the palette entries in the EGA and Tandy versions of the Freescape files allow the use of any 15 of the colours in this palette. The method in which it does so is slightly different for the EGA and Tandy versions. For the CGA display mode, further restrictions apply.

Index I R G B RGB value Remarks
0   0000 #000000
1   0001 #0000AA
2   0010 #00AA00
3   0011 #00AAAA
4   0100 #AA0000
5   0101 #AA00AA
6   0110 #AA5500 The CGA monitor included some circuitry that converted the drab dark yellow into a more vibrant brown by halving the intensity of the green channel.
7   0111 #AAAAAA
8   1000 #555555
9   1001 #5555FF
10   1010 #55FF55
11   1011 #55FFFF
12   1100 #FF5555
13   1101 #FF55FF
14   1110 #FFFF55
15   1111 #FFFFFF

As the Freescape geometry data assigns materials from 1 through 15 and uses 0 to indicate that a face is invisible, it would be impossible for Driller to have black as anything other than the background colour unless another palette entry is mapped to black. This means that effectively only 15 of the total 16 colours in the CGA palette can be used.

EGA palette data

Each of the four bytes in a palette entry is either 00 or FF, no other values are used. Byte 0 is red, byte 1 is green, byte 2 is blue, byte 3 toggles high intensity. One could calculate the index with simple bitwise operations:

palette_index = (entry [3] & 0x08) | (entry [0] & 0x04) | (entry [1] & 0x02) | (entry [2] & 0x01);

Since black is a useful colour, but material index 0 is already used to mark a face as invisible, the EGA version maps material index 1 to black. It also maps white to index 4, rather than the expected 15. Since there are only 15 indices available, one of the 16 colours in the CGA palette is unused. The colour left out is yellow (colour 14).

Tandy palette data

All four bytes in a palette entry are identical and index the palette directly:

palette_index = entry [0];

CGA palette data

CGA can display only 4 colours simultaneously in 320x200 graphics mode, selected from 2 different official 4-colour palettes (and one unofficial one that is not used by Freescape games). All the CGA palettes are available in normal and high-intensity modes. Driller and Darkside use only the normal variations of the palette, but Castle Master and Total Eclipse prefer the high-intensity modes. The resulting 4 different palette options are seen below:

Index Palette 0 Palette 1
Normal Bright Normal Bright
0 #000000   #000000   #000000   #000000  
1 #00AA00   #55FF55   #00AAAA   #55FFFF  
2 #AA0000   #FF5555   #AA00AA   #FF55FF  
3 #AA5500   #FFFF55   #AAAAAA   #FFFFFF  

Which of the two palettes is used depends solely on the lower bit of the room ID. If the room ID is odd, palette 0 is used. If the room ID is even, palette 1 is used.

As there are 15 possible material values, but only 4 possible colours in CGA graphics mode, Freescape uses dithering to distinguish the 15 different materials. the palette entries instead define dither patterns. Each byte of a palette entry defines 4 adjacent 2-bit pixel values, making for a 4x4 pixel pattern. The pixels are stored left-to-right in a byte with the leftmost pixel in the most significant two bits, and the rows are stored bottom-to-top. Driller at least only uses either solid colours or dither patterns of 2 colours, even though the 4x4 blocks could technically use all 4 colours in it. Most of those are standard checkerboards, but one of them is a bit more elaborate. For material index 12 (0x0C) it uses the pattern definition 30 0C 3C C3. In the bottom-to-top, left-to-right ordering, that produces the following pattern on screen:

Byte Pixels
3: C3        
2: 3C        
1: 0C        
0: 30        

In areas that use palette 1, the brown colour is replaced by light grey.

Room data

The structure of a room definition starts with a small header immediately followed by the object data. The structure of the header is as follows:

Offset Size in bytes Description
00 1 unknown - if this is 00, the 3D viewport is not cleared prior to drawing a new frame - this is used in entirely enclosed indoor areas
01 1 Number of objects in the room
02 1 Room ID number
03 2 16-bit offset to the room bytecode routines, relative to the start of the room data.
05 1 Room scale.
06 1 Which palette index to use as the overscan border colour by default.
07 1 Which palette index to use as the overscan border colour when under fire.
08 1 Which palette index to use as the background colour for the 3D view. This is the colour of the sky in the viewport as well as the colour for 0-bits on dithered displays such as on the ZX Spectrum.
09 1 Which palette index to use as the foreground colour for the 3D view. This the colour for 1-bits on dithered displays such as on the ZX Spectrum.
0A 2 Coordinates of the gas pocket in the room. The first byte represents the X coordinate, the second byte represents the Z coordinate (displayed as the Y coordinate to the user).
0C 1 Diameter of the gas pocket in the room. The smaller the gas pocket, the more precisely the drilling rig must be placed.
0D 12 The name of the room as a 12-character string. There is no NUL terminator. If a room name is shorter than 12 characters, it is centered by padding with spaces at the start and end of the string.
19 - The object data starts at this offset. For each of the number of objects as described in the byte at offset 01, there is an object definition. There is no terminating indicator, as the count obviates this.

As an example, the starting location in Driller is called Amethyst and its room header is located at file offset 0x9Cb4, with the objects starting at 0x9CCD with the cube that triggers the doorway to Obsidian living at 0x9D17.

Note: the indices in the room header at offsets 06 and 07 are indexed into the default CGA/EGA palette, not the altered palette defined in the game database header.

Object data

Every object has a header of at least 9 bytes, the first of which determines the type of object. All objects object types other than the entrance will have more data in an extended header which is longer than the 9-byte basic header. If the object record length in the header is larger than this (extended) header size, the object is followed by some bytecode. This bytecode is executed when the player touches or shoots the object. The basic header format is as follows:

Offset Size in bytes Description
00 1 Type selector. This is a one-byte bitfield with the following elements:
bit 7 - if set, the object is initially invisible (used to reset the in-memory data to the proper values when restarting the game)
bit 6 - if set, the object is currently invisible (in the on-disk data, this should always match the value of bit 7)
bit 5 - if set, the object has been destroyed (in the on-disk data, this should always be unset)
bits 0..4 - a 5-bit value specifying the type of object. Possible values range from 0 to 31, but the maximum value defined is 14, meaning bit 4 is effectively always 0.
01 3 The X, Y, Z coordinates of one corner of the object's axis-aligned bounding box.
04 3 The X, Y, Z dimensions of the object's axis-aligned bounding box. Adding these to the coordinates above results in the coordinates of the opposite corner of the bounding box.
07 1 Object ID number
08 1 Length of the object record, including this basic header, the information in the extended header and the optional bytecode that follows the object header.

Materials

All visible object types include in their extended header the material specifiers for each face of a 3D object or both sides of a flat object. These are stored as nibbles in a byte, though the order of the faces is apparently inconsistent. When a material is 0, the corresponding face is invisible and not rendered (in case of a sensor, it also will not fire at the player). Freescape uses this to improve performance by disabling faces which cannot possibly be seen, such as the bottom face of a cube that sits on a floor plane or the backside of a wall that bounds the current room.

The LONIBBLE() and HINIBBLE() notations used below might be implemented using the following macros in C:

#define HINIBBLE(a) (((a) >> 4) & 0x0F) #define LONIBBLE(a) ((a) & 0x0F)

Object type 0 - Entrance

Offset Size in bytes Description
00 9 The basic object header

An entrance is the odd one out of the object types. An entrance ID can (and in Driller, does) conflict with an other object ID without issue. An entrance has no visual representation itself and is therefore always marked invisible. This results in the type selector always being C0. Also, instead of bytes 4..6 defining an axis-aligned bounding box, they are interpreted as the initial orientation of the player when entering through this entrance. The values then represent rotations around the X, Y and Z axes respectively in 5-degree increments.

Because it is impossible to collide with or shoot an entrance, it does not make sense for there to be any bytecode routines attached to the object.

Object type 1 - Cube

Offset Size in bytes Description
00 9 The basic object header
09 3 Material selection for each face:
HINIBBLE(hdr [0x09]) - East (positive X)
LONIBBLE(hdr [0x09]) - West (negative X)
HINIBBLE(hdr [0x0A]) - Up (positive Y)
LONIBBLE(hdr [0x0A]) - Down (negative Y)
HINIBBLE(hdr [0x0B]) - North (positive Z)
LONIBBLE(hdr [0x0B]) - South (negative Z)

Cubes are the most basic primitives in the object types. The corner coordinates and bounding box dimensions fully define the geometry. In fact, that bounding box is the geometry.

The size of a cube's extended header is 12 bytes. If the header indicates a larger size, the remaining data is the bytecode script that is executed on collision with or shooting the cube.

Object type 2 - Sensor

Offset Size in bytes Description
00 9 The basic object header
09 1 Material selection for the sensor dot. A sensor is rendered as a single pixel and its colour is determined by LONIBBLE(hdr [0x09])
0A 1 Firing interval in 50ths of a second. A value of 50 will make the sensor fire once every second.
0B 2 Range within which the player must be for the sensor to detect them and start firing.
0D 1 Axes along which the sensor will check for line of sight to the player:
bit 5 - 0x20 - North (positive Z)
bit 4 - 0x10 - South (negative Z)
bit 3 - 0x08 - Up (positive Y)
bit 2 - 0x04 - Down (negative Y)
bit 1 - 0x02 - East (positive X)
bit 0 - 0x01 - West (negative X)

Sensors are the primary hazards encountered in Driller as they will continuously shoot at the player at regular intervals as long as the player is within range and they have direct line of sight to the player. The distance is calculated not as a Euclidian distance (i.e. with the Pythagorean theorem), but as a Manhattan distance, simply adding the absolute value of the differences in the x, y and z coordinates together, leading to the target volume being a square-based pyramid with its center point at the sensor coordinates and the tip pointing directly along each axis that the sensor can sense.

The size of a sensor's extended header is 14 bytes. If the header indicates a larger size, the remaining data is the bytecode script that is executed on shooting the sensor. Yes, it is possible, if exceedingly difficult, to shoot that single pixel.

Object type 3 - Rectangle

Offset Size in bytes Description
00 9 The basic object header
09 1 Material selection for each side: HINIBBLE(hdr [0x09]) - Front face LONIBBLE(hdr [0x09]) - Back face

A rectangle's bounding box should be flat, i.e. exactly one of its dimensions should be 0. When the X dimension is zero, the plane of the rectangle is perpendicular to the X axis, its front face material being applied to the eastern (positive X) side of the rectangle and the back face material being applied to the western (negative X) side of the rectangle. Analogously, for a rectangle perpendicular to the Y axis, the front face material is applied to the upper (positive Y) side, and the back face material is applied to the lower (negative Y) side. And for a rectangle perpendicular to the Z axis, the front face material is applied to the northern (positive Z) side, and the back face material is applied to the southern (negative Z) side of the rectangle.

Note: If the bounding box has all non-zero dimensions and is thus a rectangular cuboid, the rectangle is rendered as a slope at an angle with the plane of the polygon being parallel to the X axis (its lower edge extends from the base corner along the positive X direction). In that case, when the player is at a Z coordinate greater than (i.e. north of) the base corner, it is rendered in the front face material, otherwise it is rendered in the back face material. This implies that the engine does its material selection as though it were a rectangle perpendicular to the Z axis.

The size of a rectangle's extended header is 10 bytes. If the header indicates a larger size, the remaining data is the bytecode script that is executed on collision with or shooting the rectangle.

Object types 4..9 - Pyramid

Offset Size in bytes Description
00 9 The basic object header

Object types 10..14 - Polygons

Offset Size in bytes Description
00 9 The basic object header

Script data

Although the main gameplay in the Freescape games appears to be hardcoded into the engine, each object can also have an associated bytecode script, which is executed when the player either touches or shoots an object. As an object can only have a single script, it actually uses the same script for both, with the bytecode itself having a flag per instruction to distinguish between either case. Rooms can also have associated scripts, which run every script tick. And finally there can be global bytecode scripts, too. These are also run every script tick, regardless of which room the player is in. Global scripts and room scripts are specified in a table which can theoretically have up to 255 entries.

There is no "end of script" marker. In the case of object scripts, the length of the bytecode data is determined by taking the total length of the object record and subtracting the size of the object header. For global and room scripts, each bytecode table starts with a one-byte value specifying the number of routines in the bytecode table. This count is followed immediately be the routines themselves. Each routine in turn starts with a one-byte value specifying the length of the routine in bytes, followed by that exact number of bytes for the actual bytecode data.

The scripts are extremely limited in what they can do, with little or no flow control, math, nor access to player or object coordinates or indeed anything but the most basic checks. The few conditions that can be checked will either result in the script continuing or stopping execution. They do, however, make it possible to toggle other objects' visibility or destroy them, even in rooms other than the current ones. Another major use for the bytecode scripts is having objects act as doorways into other rooms. Some of the available instructions seem to be game-specific at least for Driller, though I've not evaluated those for the other games. Despite the severe limitations, the level of interactivity that could be achieved is pretty impressive.

Game state

There are 256 variables and 256 bit flags available to the bytecode interpreter. This does not include the player score, shield or energy levels (the probe and the jet have separate energy and shield levels), nor any positional data, not even the player position. The various opcodes that reference these flags or variables do so by index, with the index itself being an 8-bit byte value. A flag can be either 0 (false) or 1 (true). A variable can have any value from 0x00 to 0xFF. Whether the values are treated as signed or unsigned is irrelevant, as the only operations available on variables are incrementing or decrementing, setting to a specific value and checking against a constant value for equality. These operations perform identically regardless of whether the values are signed or unsigned.

Very few of the variables appear to actually be used by the bytecode. Certain variables and flags will appear to have a special meaning and seem to be set by the main game logic to communicate certain events to the bytecode scripts. I've been able to discern for Driller:

Variable(s) Function
1E Every time the minute display on the clock changes (which coincides with a forced execution run of the room and global scripts), variables 1E and 1F are incremented.
1F Is incremented at the same time as variable 1E, but is reset to zero upon room change and by a bytecode script after reaching the value 5. It ends up corresponding to the number of times the minute display on the clock has changed since entering the current room, ranging from 0..5. This is used by the first and second global scripts. The first global script makes the orbital Skanner visible whenever variable 31 has the value 4 and the second global scripts hides the orbital skanner when the variable has the value 5 and then resets it to 0. This results in the skanner appearing 3-4 minutes after entering a room, then disappearing exactly one minute later.
27..2E The ASCII text string of the current clock time in the format HH:MM:SS, including the semicolons in variables 29 and 2C.

Opcodes

For object scripts, the upper bit 0x80 is set when the opcode is to be interpreted when an object is shot, rather than touched. When the bit is clear, the opcode is to be interpreted only when the object is touched, rather than shot. This allows for two separate behaviours for a single object, despite it having just a single associated bytecode script.

Each parameter is a single byte. These usually represent IDs or indices and should be interpreted as 8-bit unsigned values unless otherwise noted.

Byte Command Parameters Description
00 NOP - Does nothing, can be used as a filler byte.
01 +SCORE a b c Increase the player's score. The three parameter bytes are considered a 24-bit little-endian signed integer. Negative values can thus be used to decrease the player's score. Assuming an integer size larger than 24 bits, one possible way of converting to a signed value is: delta = (c << 16) + (b << 8) + a; if (delta & 0x800000) delta -= 0x1000000; score += delta;
02 +ENERGY n Increase the player's energy level by n units. This is a signed 8-bit value and negative values can thus be used to decrease the player's energy level.
03 TOGVIS obj Toggle the visibility of the specified object in the current room.
04 VIS obj Show the specified object in the current room.
05 INVIS obj Hide the specified object in the current room.
06 RTOGVIS room obj Toggle the visibility of the specified object in another room.
07 RVIS room obj Show the specified object in another room.
08 RINVIS room obj Hide the specified object in another room.
09 INCR var Increment the variable at index var by one.
0A DECR var Decrement the variable at index var by one.
0B IFEQ var value Continue script execution only if the variable at index var has the value value, otherwise stop execution.
0C SETBIT bit Set the flag at index bit to 1 (true).
0D CLRBIT bit Set the flag at index bit to 0 (false).
0D IFBIT bit Continue script execution only if the flag at index bit is set to 1 (true), otherwise stop execution.
0F SOUND n Play the designated sound immediately. This results in the display being updated first, then the sound being played and the order of operations is therefore the inverse of the SYNCSND opcode.
10 DESTROY obj Destroy the specified object in the current room.
11 RDESTR room obj Destroy the specified object in another room.
12 GOTO room entrance Teleport the player into the specified room, to the entrance with the matching ID.
13 +SHIELD n Increase the player's shield level by n units. This is a signed 8-bit value and negative values can thus be used to decrease the player's shield level.
14 SETV var value Set the variable at index var to value value.
15 SWAPJET - Switch between the excavation probe and the reconnaissance jet.
16 CLIMB Opcodes 16 .. 18 appear to be unused.
17 FALL
18 EXTEND
19 SPFX xy Special effects. The single parameter byte contains two 4-bit values. HINIBBLE(xy) is the index of the colour entry to change and LONIBBLE(xy) is the value to set it to. These appear to be
20 REDRAW - Redraw the screen.
21 DELAY n Wait for n 50ths of a second.
22 SYNCSND n Play the designated sound on the next frame update, including when the REDRAW opcode is issued. This results in the display being updated first, then the sound being played and the order of operations is therefore the inverse of the SOUND opcode.
23 TOGBIT bit Toggle the flag at index bit between 0 (false) and 1 (true).
24 IFVIS obj Continue script execution only if the specified object is currently visible, otherwise stop execution.
25 IFINVIS obj Continue script execution only if the specified object is currently not visible, otherwise stop execution.

Demo data

Driller runs a gameplay demo when the game is left on the title screen after a game session (but not if this is done before starting at least one game). The gameplay demo is a simple list of input commands that are carried out sequentially. A command may be executed once, taking one game tick (1/10th of a second). This is useful for things like teleporting in a drilling rig, making a U-turn or firing the lasers. Alternatively it may be repeated multiple times. For things like rotation or movement, the command would then be repeated for the specified number of game ticks. For things like moving the targeting cursor, the repeat count simply specifies by how many pixels to move the cursor, taking 0 time.

The demo data is just a stream of bytes. The stream ends when a NUL byte is encountered. If a byte has the high bit clear, it is a command to be executed. If a byte has the high bit set, the lower seven bits determine the repeat count. If the lower seven bits are all 0, this is actually interpreted as being 255. All other values should be increased by 1. A command byte will always follow the repeat counter immediately.

Pseudo-code to decode the RLE compressed demo data. repeat_count = 1; command = readbyte(); if (command & 0x80) { repeat_count = (command & 0x7F) + 1; if (repeat_count == 1) repeat_count = 255; command = readbyte(); }

Not all commands seem to have been defined or applicable. The ones I've been able to determine are:

Byte What it does Keyboard equivalent
00End of demo - this ends the demo-
01RaiseR
02FallF
03Move forwardO or in movement mode
04Move backK or in movement mode
05Rotate leftQ or in movement mode
06Rotate rightE or in movement mode
07Look upP
08Look downL
09Roll leftN
0ARoll rightM
0BIncrease angleA
0CDecrease angleZ
0DU-turnU
Commands 0E .. 10 appear to be unused.
11Increase stepX
12Decrease stepS
Commands 13 .. 15 appear to be unused.
16Fire lasers5 on the numeric keypad
17Move crosshairs right (repeat count is used as a number of pixels)O or in targeting mode
18Move crosshairs left (repeat count is used as a number of pixels)K or in targeting mode
19Move crosshairs down (repeat count is used as a number of pixels)Q or in targeting mode
1AMove crosshairs up (repeat count is used as a number of pixels)E or in targeting mode
Commands 1B .. 1D appear to be unused.
1ESwitch between movement and targeting modesSpace
Commands 1F .. 27 appear to be unused.
28DrillD
29Info screen. If this is done in the demo, there doesn't appear to be a way out of it.I
7FNOP - does nothing, can be used as a filler byte-