AGOS VGA Format
Format type | Tileset |
---|---|
Hardware | VGA |
Max tile count | Unlimited |
Palette | External, probably inside the odd-numbered file. |
Tile names? | No |
Minimum tile size (pixels) | 0×0 |
Maximum tile size (pixels) | 65535×32767 |
Plane count | 1 |
Plane arrangement | Linear |
Transparent pixels? | No |
Hitmap pixels? | No |
Metadata? | None |
Supports sub-tilesets? | No |
Compressed tiles? | Yes |
Hidden data? | Yes |
Games |
The .VGA files used by the AdventureSoft / HorrorSoft games running on the AGOS engine always come in numbered pairs: a file for an odd number, and a file for the following even number. For example, 011.VGA and 012.VGA. In some games, another odd-numbered file follows these two, forming groups of three files.
The format of the odd-numbered files is still largely unknown. They are said to contain colour palettes for the sprites and other scene information for the game. Further research in the ScummVM code might reveal more about them.
The even-numbered files contain sprites. Each file is a collection of 16-colour images with individual dimensions, and with support for RLE compression. The frames list can contain 0×0 entries. Different frames inside one file very often require a different colour palette.
Header
The file's header is an index table of frames information. It contains no additional information; there are no easily identifiable magic numbers, nor does the file contain an indication of the amount of frames in the file.
Each entry has the following structure:
Offset | Data type | Name | Description |
---|---|---|---|
0x00 | UINT32LE | DataOffset | Offset in the file where the image data is located. |
0x04 | UINT16LE | ImageHeight | Image height. The highest bit indicates that compression is enabled, and should be cleared from the height value. |
0x06 | UINT16LE | ImageWidth | Image width. |
The index can contain empty frames, with zero for all three values. A lot of the files in this format start with such an empty frame, but this is not a requirement for the format.
The end of the header, and thus the amount of frames, can be determined from the DataOffset
of the first non-empty frame in the file.
Frame handling
The images are 4-bit, and use a minimum stride for their data, meaning each line of pixels has a length of ((ImageWidth * 4) + 7) / 8
bytes, and the full length is that stride multiplied by ImageHeight
.
In case there is no compression, the data at DataOffset
will simply be the 4-bit image data.
Compression
When the compression flag is set, the data is compressed with a Code-based RLE with 1-byte codes, on which the highest bit indicates a Copy command when enabled, and a Repeat command when disabled. When the value is a Repeat command, the amount to repeat is Code + 1
. When it is a Copy command, the amount to copy is 0x100 - Code
.
The result of the decompression is put in the resulting buffer vertically, column by column, and since it is a 4-bit format, each such column represents two pixels on the image.
There is no real indication of where the compressed data ends, but since the output buffer length is known, the decompression can simply end when the buffer has been filled up. An additional check on the size can be done using the next non-zero offset from the index table.
Example compression / decompression code
C#
This is written as subclass of the C# RLE basis on the main RLE article, overriding the code reading and writing functions, and the maximum repeat and copy values required for the compression process. Since the basic RLE class writes to its buffer linearly, whereas AGOS RLE works per column, this code contains an extra post-processing on the decode, and pre-processing on the encode, to swap the rows and columns.
public class AgosCompression: RleImplementation<AgosCompression>
{
public static Byte[] DecodeImage(Byte[] buffer, UInt32? startOffset, UInt32? endOffset, Int32 height, Int32 stride)
{
AgosCompression rle = new AgosCompression();
Int32 byteLength = stride * height;
Byte[] outBuffer = new Byte[byteLength];
if (rle.RleDecodeData(buffer, startOffset, endOffset, ref outBuffer, true) == -1)
return null;
// outBuffer is now the image, with its columns stored as rows.
Byte[] outBuffer2 = new Byte[byteLength];
// Post-processing: Exchange rows and columns.
for (Int32 i = 0; i < byteLength; i++)
outBuffer2[i % height * stride + i / height] = outBuffer[i];
// outBuffer2 is now the correct image.
return outBuffer2;
}
public static Byte[] EncodeImage(Byte[] buffer, Int32 stride)
{
Int32 byteLength = buffer.Length;
Int32 height = byteLength / stride;
// Should not happen, but you never know...
while (byteLength > height * stride)
height++;
Byte[] buffer2 = new Byte[byteLength];
// Pre-processing: Exchange rows and columns.
for (Int32 i = 0; i < byteLength; i++)
buffer2[i] = buffer[i % height * stride + i / height];
// buffer2 is now the image, with its columns stored as rows.
// Perform actual compression.
AgosCompression rle = new AgosCompression();
return rle.RleEncodeData(buffer2);
}
#region tweaked overrides
/// <summary>Maximum amount of repeating bytes that can be stored in one code.</summary>
public override UInt32 MaxRepeatValue { get { return 0x80; } }
/// <summary>Maximum amount of copied bytes that can be stored in one code.</summary>
public override UInt32 MaxCopyValue { get { return 0x7F; } }
/// <summary>
/// Reads a code, determines the repeat / skip command and the amount of bytes to repeat/skip,
/// and advances the read pointer to the location behind the read code.
/// </summary>
/// <param name="buffer">Input buffer.</param>
/// <param name="inPtr">Input pointer.</param>
/// <param name="bufferEnd">Exclusive end of buffer; first position that can no longer be read from.</param>
/// <param name="isRepeat">Returns true for repeat code, false for copy code.</param>
/// <param name="amount">Returns the amount to copy or repeat.</param>
/// <returns>True if the read succeeded, false if it failed.</returns>
protected override Boolean GetCode(Byte[] buffer, ref UInt32 inPtr, ref UInt32 bufferEnd, out Boolean isRepeat, out UInt32 amount)
{
if (inPtr >= bufferEnd)
{
isRepeat = false;
amount = 0;
return false;
}
Byte code = buffer[inPtr++];
isRepeat = (code & 0x80) == 0;
amount = (UInt32)(isRepeat ? code + 1 : 0x100 - code);
return true;
}
/// <summary>
/// Writes the copy/skip code to be put before the actual byte(s) to repeat/skip,
/// and advances the write pointer to the location behind the written code.
/// </summary>
/// <param name="bufferOut">Output buffer to write to.</param>
/// <param name="outPtr">Pointer for the output buffer.</param>
/// <param name="bufferEnd">Exclusive end of buffer; first position that can no longer be written to.</param>
/// <param name="forRepeat">True if this is a repeat code, false if this is a copy code.</param>
/// <param name="amount">Amount to write into the repeat or copy code.</param>
/// <returns>True if the write succeeded, false if it failed.</returns>
protected override Boolean WriteCode(Byte[] bufferOut, ref UInt32 outPtr, UInt32 bufferEnd, Boolean forRepeat, UInt32 amount)
{
if (bufferOut.Length <= outPtr)
return false;
if (forRepeat)
bufferOut[outPtr++] = (Byte)(amount - 1);
else
bufferOut[outPtr++] = (Byte)(0x100 - amount);
return true;
}
#endregion
}
Tools
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 |
---|---|---|---|---|---|---|---|
Engie File Converter | Windows | Yes | Yes | Yes | No | N/A | Only reads the even-numbered sprite files. Its frame export writes empty files for 0×0 frames. |