Monster Bash Sprite Format
A Monster Bash sprite contains multiple 16-colour EGA images (for example, an enemy sprite contains all the animation frames for that sprite.) This format is trickier than most, as it contains an arbitrary number of colour planes - the EGA contains four colour planes (red, green, blue and intensity) but each plane in the Monster Bash sprite can refer to multiple EGA planes at the same time (so you could have a "magenta plane" in the file, which puts data into the EGA red and blue planes at the same time!)
All sprite files begin with a single 0xFF byte. The games never checks this value and just skips past it. To correctly detect this file format, the following tests can be used:
- The last image block should end at the end of the file, there should be no trailing data (except a possible trailing 0x00 depending on the decompressor used)
- The last byte in each image block should be 0x00 to signal the final plane of image data
A number of image blocks immediately follow the signature byte, repeating until EOF:
|UINT16 iSize||Size of this sprite|
|BYTE cData[iSize]||Block of image data, iSize bytes long|
Each of the blocks of image data in the above structure contains an image header, followed by multiple planes of data. The header looks like this:
|UINT8 iHeight||Height of the image in pixels|
|UINT8 iWidth||Width of the image in pixels|
|UINT8 iReserved||Must always be set to zero (The game occasionally treats iWidth as a UINT16LE)|
|INT16LE iHotspotX||Image is offset by this many pixels (see below)|
|INT16LE iHotspotY||Image is offset by this many pixels|
|UINT16LE iRectX||Right-coordinate of collision rectangle (object position is left-coordinate)|
|UINT16LE iRectY||Bottom-coordinate of collision rectangle (object position is top-coordinate)|
The upper four bits of iFlags can be one or more of these values:
|2||Set if the sprite is wider than 64 pixels (tells engine to use fallback drawing routine)|
The lower four bits of iFlags specify the number of shifts a sprite has. These make drawing graphics faster on non-byte boundaries. Most sprites have a value of 1 here (no shifts, just the original image) which makes the game perform any shift at drawing time. Sprites that need to be drawn frequently (such as projectiles) typically have a higher value here (2, 4 or 8.) This causes the game to generate this many shifts when loading the sprite, to speed up drawing at the expense of taking up more memory. The "rock" sprite is one of the few that have frames with a shift value other than 1.
If a sprite has a shift count greater than 1, the game expects the iPlaneBits value of the first image block to be the same as the width (in bytes) of the image. The game uses this value to calculate iPlaneSize as iWidth * iPlaneBits. This value is then used to calculate total number of image planes for the current sprite, so that the code knows how big the buffer for the shifted copies must be. This quirk means that the color planes for shifted sprites cannot be chosen as freely as those for non-shifted sprites, possibly increasing the size of the sprite data by another plane.
The "object position" as tracked by the game is the top-left corner of the collision rectangle. iRectX and iRectY indicate the bottom-right corner of the collision rectangle. iHotspotX and iHotspotY indicate how far up and to the left the image should be shifted. These (usually negative) values should be added to the object location to draw the sprite frame at the correct location. For instance if iHotspotX is -5, then the image is shifted left by five pixels, so the object location is at x=5 (five pixels into the image). Tweaking these variables allows the collision rectangle to be set arbitrarily, with the only restriction being that the object location tracked by the game will always be part of the collision rectangle.
After this header, a number of data planes follow one after the other. Each plane of data can contain the image data for multiple EGA planes. For example, if EGA plane 1 is blue and EGA plane 2 is green, then a single Monster Bash plane can have a bit flag for 3, which means the data is for EGA plane 1 and 2 (
1 | 2 == 3).
The very first plane in the image is always transparency, and the iPlaneBits has special meaning here - it is set to the width of the image, in bytes. This can be calculated with the formula width_bytes = (width_pixels + 7) / 8. Each plane is of the following structure:
|UINT8||iPlaneBits||Which colours this plane holds (or image width in bytes, for transparency plane)|
|BYTE[iPlaneSize]||cPlaneData||Plane data, one bit-per-pixel|
iPlaneSize is the size of the data in the plane. In order to calculate this, you must take the width of the image (in pixels, obtained from the frame's header above) and round this up to the nearest byte boundary. For example, if the image is eight pixels wide, it will take up eight bits which fits in one byte. If the image is nine pixels wide, this will take up nine bits, so two bytes are required to hold each scanline of data. The plane size is then the number of bytes in a scanline multiplied by the image height. You can use a formula like one of the following to calculate the various values, where width and height are the image dimensions in pixels:
bits per scanline = width + (8 - (width % 8)) bytes per scanline = (width + 7) / 8 bytes per plane = bytes per scanline * height
An image that's 56x99 pixels, should have a plane size of 693 bytes (7 bytes per scanline.)
iPlaneBits indicates which EGA planes the data applies to. If this value is zero the end of the data has been reached. If for example the value is 0x06, this means the plane should be drawn on EGA plane 0x02 and EGA plane 0x04.
This means a given EGA plane (say red, or green) can be updated by multiple image planes. To handle this, each plane's data should be applied using an XOR operation, so that a one-bit in the incoming plane will flip the bit for that colour in the image (so it may get flipped on in one plane, but flipped off again when the next plane is read in.)
Once the image plane identified by 0x00 has been reached, the rendering of the image is complete. There is no data following iPlaneBits in this case.
Note that it is possible to set a colour for a transparent pixel. If this is done (i.e. a transparent pixel is not set to black in the other planes) the value will be combined (via XOR) with whatever is behind it. To get proper transparency, those pixels that are transparent should always be set to black.
- The number of image planes is not stored in the file. The only known way of precalculating this is by working it out from the size of the image. The alternative of simply reading in image planes until one for bit 0 is reached is also undesirable, as a corrupt image could result in the program reading far too much data out of the file. Even a rough calculation of the number of image planes will act as a safeguard against this.
- If an image is not a multiple of eight pixels across, the last byte on each row will contain unused pixels. These pixels must be set to black and marked as transparent, otherwise the game will draw them but fail to erase them as they are past the edge of the image. This will leave a trail of visual artefacts as the sprite moves around.
The following tools are able to work with files in this format.
|Name||Platform||View images in this format?||Convert/export to another file/format?||Import from another file/format?||Access hidden data?||Edit metadata?||Notes|
This file format was reverse engineered by Malvineous. Key parts (shifts, flags, transparency planebits special case) were reverse engineered by Lemm. If you find this information helpful in a project you're working on, please give credit where credit is due. (A link back to this wiki would be nice too!)