Xeen Wiki

Apart from RAW images, all graphics used by the Xeen engine are stored in a custom format.

Usage[]

RAW images, as mentioned above, hold many of the background images used by the game engine. Every other graphic - including a handful of background images, such as the death sequence background - is stored in this sprite file format.

Whereas a RAW image can only hold one full-screen image, a sprite can hold many images and also supports transparency. This makes the sprite file ideal for holding animation sequences, like the attack animations of monsters, or even a collection of similar images, like buttons used in the GUI. The mouse pointer graphic is also stored as a sprite.

File Format[]

File Header[]

A sprite file contains two blocks of data: the file header that tells you how many frames there are and which cells are combined to form a frame, and the individual cells.

Offset Length Description
0000h 2 The number of frames in the sprite. This is a word with the LSB first.
0002h n * 4 The cells that are used to generate a frame where n is the number of frames in the sprite. These are also the offsets in the sprite file where the cell data begins. Each frame can be a combination of up to two cells - one drawn over top the other. The first two bytes is the offset to the first cell and the second two bytes is the offset of the second cell. The first offset is never zero (meaning there is always one cell that makes up a frame), but the second offset can be zero if all the image data was stored in the first cell.


Cell Data[]

Once you have determined the number and offset of the cells in the sprite file, you can begin to load them one at a time. Like the sprite file itself, each block of cell data has a header which is followed by the compressed image data.

Offset Length Description
0000h 2 x-offset - skip this many pixels at the beginning of each scan line filling them with the transparent color.
0002h 2 width - the number of pixels that will be drawn on each scan line, but not necessarily the width of the entire cell.
0004h 2 y-offset - the number of scan lines to skip before beginning to draw the cell.
0006h 2 height - the total number of scan lines to draw using the following compressed image data, but not necessarily the height of the entire cell.
0008h variable The compressed image data that is used to draw this cell.

To determine the width of a cell add the x-offset and width. To determine the height of a cell add the y-offset and height.

Decompressing the Image Data[]

Color data in a sprite cell is stored one line at a time. The first byte of each line is the length of the data that makes up that particular scan line (excluding that byte itself). If the length byte is zero then the second byte is the number of scan lines to skip (a y-offset) and fill with the transparent color. If the length byte is non zero then the second byte is the number of pixels to skip on that scan line (a x-offset) before drawing. Every line contains at least two bytes of data, but most will contain more.

Following the first two bytes are the compressed image data for the scan line. The compression uses a opcode/parameter(s) format that is similar to bitmap RLE compression, but more complex. The next byte in the sequence contains the drawing command and a length value. The drawing command is stored in the three most significant bits of the byte and the length value is stored in the bottom five bits of the byte. For example:

Opcode Command Length Description
04h (00000100b) 0 4 The following 5 byte should be used to draw 5 pixels
41h (01000010b) 2 1 The following byte should be used to draw 4 pixels
A3h (10100011b) 5 3 Skip the next 4 pixels filling with the transparent color

The examples above probably don't mean much to you now, but will all be explained later. For now it is enough to know that the top 3 bits are the drawing command the bottom 5 are the length.

Drawing Commands[]

To get the length and drawing command from the opcode byte, perform the following operation:

len = opcodeByte & 1Fh
cmd = ( opcodeByte & E0h ) >> 5

Where & is the bitwise AND operator and >> is the bitwise SHIFT operator. The following table lists the possible drawing commands and their description.

Command Description
0 The following len + 1 bytes are stored as indexes into the color table.
1 The following len + 33 bytes are stored as indexes into the color table.
2 The following byte is an index into the color table, draw it len + 3 times.
3 Stream Copy command.
4 The following two bytes are indexes into the color table, draw the pair len + 2 times.
5 Skip len + 1 pixels filling them with the transparent color.
6 & 7 Pattern command.

Stream Copy Command[]

When you encounter the stream copy command immediately read a WORD from the image data. Use that word as a negative offset into the compressed color data and read len + 4 bytes. Each byte you read should be used as an index into the color pallet to draw one pixel. When you are reading from a previous part of the byte stream the data no longer contains any meaning. That is to say they are no longer opcodes and parameters, but simply indexes into the color table regardless of how they were used the first time you encountered them.

Pattern Command[]

This is the most complicated drawing command.

The command byte is broken down in to:

  • Key: ((cmd >> 2) & 0x0E)
  • Count: ((cmd & 0x07) + 3)

The key is then used to read two values from the following array (the PatternSteps array):

0, 1, 1, 1, 2, 2, 3, 3, 0, -1, -1, -1, -2, -2, -3, -3
  • Change1 = PatternSteps[Key]
  • Change2 = PatternSteps[Key+1]

For example, a Key of zero would return a Change1 of 0 and a Change2 of 1. A Key of 8 would return a Change1 of 0 and a Change2 of -1.

Additionally, one byte of data is read from the line.

  • Value = data byte

Now, Count bytes are written to the image, following this pattern:

  1. Write the Value byte to the image.
  2. If more bytes need to be written, continue, otherwise stop.
  3. Add Change1 to the Value byte.
  4. Write the new Value byte to the image.
  5. If more bytes need to be written, continue, otherwise stop.
  6. Add Change2 to the Value byte.
  7. Write the new Value byte to the image.
  8. If more bytes need to be written, begin from the start, otherwise stop.

Sample Code[]

// This macro draws a color in the pallet at colorIndex to the x,y coordinates in the raster
#define putPixel( x, y, colorIndex ) \
  colorData[(y*width+x)*4+0] = (BYTE)( palletData ? palletData[colorIndex*3+2] << 2 : colorIndex ); \
  colorData[(y*width+x)*4+1] = (BYTE)( palletData ? palletData[colorIndex*3+1] << 2 : colorIndex ); \
  colorData[(y*width+x)*4+2] = (BYTE)( palletData ? palletData[colorIndex*3+0] << 2 : colorIndex )

// Uncompress a cell color data into a 32-bit raster
//  * cellData     - pointer to the cell's color data
//  * palletData   - pointer to the pallet data as loaded from the .cc file or NULL
//  * colorData    - pointer to a byte array that will contain the RGB conversion of the cell
//  * transparent  - the color to use as transparent
void cellToRgb32( BYTE *cellData, BYTE *palletData, BYTE *colorData, COLORREF transparent )
{
  DWORD i, dp = 0;                          // Pointer within data stream
  DWORD xPos, yPos;                         // The position within the color raster
  DWORD lineLength, byteCount;              // total bytes/bytes read in this scan line
  DWORD opcode, cmd, len, opr1, opr2;       // Used to decode the drawing commands
  DWORD xOffset, yOffset, width, height;    // Cell position and size

  // The pattern steps used in the pattern command
  int patternSteps[] = { 0, 1, 1, 1, 2, 2, 3, 3, 0, -1, -1, -1, -2, -2, -3, -3 };

  // Read the position and size of the cell from the data stream
  xOffset = *((WORD *)(&cellData[0])); dp += 2;
  width   = *((WORD *)(&cellData[2])); dp += 2;
  yOffset = *((WORD *)(&cellData[4])); dp += 2;
  height  = *((WORD *)(&cellData[6])); dp += 2;

  // Fill the color raster with the transparent color
  for( i = 0 ; i < ( xOffset + width ) * ( yOffset + height ) ; i++ )
  {
    *((DWORD *)(&colorData[i * 4])) = transparent;
  }

  for( yPos = yOffset, byteCount = 0 ; yPos < height + yOffset ; yPos++, byteCount = 0 )
  {
    // The number of bytes in this scan line
    lineLength = cellData[dp++];

    if( lineLength > 0 )
    {
      // Skip the transparent color at the beginning of the scan line
      xPos = cellData[dp++] + xOffset; byteCount++;

      while( byteCount < lineLength )
      {
        // The next byte is an opcode that determines what 
        // operators are to follow and how to interpret them.
        opcode = cellData[dp++]; byteCount++;

        // Decode the opcode
        len = opcode & 0x1F;
        cmd = ( opcode & 0xE0 ) >> 5;

        switch( cmd )
        {
          case 0:   // The following len + 1 bytes are stored as indexes into the color table.
          case 1:   // The following len + 33 bytes are stored as indexes into the color table.
            for( i = 0 ; i < opcode + 1 ; i++, xPos++ )
            {
              opr1 = cellData[dp++]; byteCount++;
              putPixel( xPos, yPos, opr1 );
            }
            break;

          case 2:   // The following byte is an index into the color table, draw it len + 3 times.
            opr1 = cellData[dp++]; byteCount++;
            for( i = 0 ; i < len + 3 ; i++, xPos++ )
            {
              putPixel( xPos, yPos, opr1 );
            }
            break;

          case 3:   // Stream copy command.
            opr1 = *((WORD *)&cellData[dp]);
            dp += 2; byteCount += 2;
            for( i = 0 ; i < len + 4 ; i++, xPos++ )
            {
              opr2 = cellData[dp-opr1+i];
              putPixel( xPos, yPos, opr2 );
            }
            break;

          case 4:   // The following two bytes are indexes into the color table, draw the pair len + 2 times.
            opr1 = cellData[dp++]; byteCount++;
            opr2 = cellData[dp++]; byteCount++;
            for( i = 0 ; i < len + 2 ; i++, xPos += 2 )
            {
              putPixel( xPos+0, yPos, opr1 );
              putPixel( xPos+1, yPos, opr2 );
            }
            break;

          case 5:   // Skip len + 1 pixels filling them with the transparent color.
            xPos += len + 1;
            break;

          case 6:   // Pattern command.
          case 7:
            // The pattern command has a different opcode format
            len = opcode & 0x07;
            cmd = ( opcode >> 2 ) & 0x0E;

            opr1 = cellData[dp++]; byteCount++;
            for( i = 0 ; i < len + 3 ; i++, xPos++ )
            {
              putPixel( xPos, yPos, opr1 );
              opr1 += patternSteps[cmd + ( i % 2 )];
            }
            break;
        }
      }
    }
    else
    {
      // Skip the specified number of scan lines
      yPos += cellData[dp++];
    }
  }
}

Drawing Frames[]

As an example of how cells combine to form frames, take the following monster attack animation, 002.ATT from Darkside of Xeen:

First Cell Second Cell Result
DARK.CC-022.ATT-MM4.PAL.Cel0
Cell 0
+ DARK.CC-022.ATT-MM4.PAL.Cel1
Cell 1
= DARK.CC-002.ATT-MM4.PAL.Frame0
Frame 0
DARK.CC-022.ATT-MM4.PAL.Cel0
Cell 0
+ DARK.CC-022.ATT-MM4.PAL.Cel2
Cell 2
= DARK.CC-002.ATT-MM4.PAL.Frame1
Frame 1

As the above images show, by "sharing" common pixels between frames, it reduces the amount of redundant information that must be stored in the file. The common pixels are stored in the first cell, and then the different pixels are displayed when the second cell is drawn over the top

Please note that the cells are not combined in an arbitrary manner: Frame 0 does NOT always consist of cells 0 and 1, Frame 1 does not always consist of cells 0 and 2.

If the two cells of a frame have different height the height of the frame is the height of the taller cell. In that case the two cells must align at the top.