Treasure Mountain Panning Image Format
Format type | Tileset |
---|---|
Hardware | EGA |
Max tile count | - Sub-images: depends on compression and addressing limits; compressed tile maps block cannot exceed 65535 bytes. - 8×8 tiles: 2047. |
Palette | External |
Tile names? | No |
Minimum tile size (pixels) | 8×8 |
Maximum tile size (pixels) | 524280×524280 |
Plane count | 1 |
Plane arrangement | Linear |
Transparent pixels? | No |
Hitmap pixels? | No |
Metadata? | None |
Supports sub-tilesets? | No |
Compressed tiles? | Yes |
Hidden data? | No |
Games |
The PAN Format is used in the game Treasure Mountain to store multiple 16-color graphics images built up from 8×8 tiles. The images are stitched together to be used as panning backgrounds.
File format
Header
The file format starts with the folowing 8-byte header:
Offset | Data type | Name | Description |
---|---|---|---|
0x00 | UINT16LE | TileMapsDataSize | Length of the tile maps data block inside the data. |
0x02 | UINT16LE | TilesWidth | Width in tiles of the contained images. The final width of the images will be eight times this value. |
0x04 | UINT16LE | TilesHeight | Height in tiles of the contained images. The final height of the images will be eight times this value. |
0x06 | UINT16LE | ImageDataSize | Length of the image data block inside the data. |
Immediately after this comes the data. This consists of two parts: first the tile maps data part, then the image data part. The tile maps data contains instructions for building up the final image frames from that image data. The image data is a compressed block containing the data for all consecutive 8×8 image tiles. The TileMapsDataSize and ImageDataSize define the length of each of these blocks, meaning they can be used to determine where the tile maps data ends and the image data starts. Since both types of data use some sort of compression, no information about the exact amount of frames or image tiles is known before the data in its respective data block is completely processed.
Tileset image data
The image data is in 4bpp linear format, big-endian packed, meaning that the bytes AB CD would represent four pixels, left to right, of colour values using indices 10, 11, 12 and 13 on the palette.
The full image data of all tiles is stored as one large compressed block of data, which should be decompressed at once, not as separate tiles, since repeats can cross over to the data of next tiles. The used compression is a flag-based RLE compression with flag value 0xA5 (165); when encountered, the two bytes behind it are respectively the amount of times to repeat, and the value to repeat. The compression uses escaping for its flag value: to save space, a single value 0xA5 by itself (two pixels with colour indices 10 and 5) is stored as A5 A5, rather than simply as a "repeat of one" as A5 01 A5.
This would theoretically mean that an actual repeat of 165 bytes of value 0xA5 would need to be split up, however, even though compressed blocks can cross over to later tiles, the format still only stores unique tiles, which means that the longest possible repeat inside the data that doesn't result in duplicate tiles would be three tiles in a row, where the first byte of the first tile and the last byte of the last tile differ, and everything in between is the same. Since these are 4-bit 8×8 tiles, so 32 bytes each, this would still only give a range of 31 + 32 + 31 = 94, which is far below the actual flag value.
Since the final data contains one tile after the next, the correct way to interpret the final decompressed result is as one long image that is 8 pixels wide, and as long as the data allows. When seen that way, the offsets given in the tile maps data can be transformed into Y-offsets that indicate where a requested 8x8 tile starts.
As example, this is the dumped tileset of MOUNTAIN.PAN, shown tiled with 24 tiles per row:
Since the game accepts repeats of 0 bytes long, technically, data can be hidden inside the compressed image data by prefixing each byte with bytes A5 00.
The image data stored inside the existing files seems to be reordered in some way that makes the compressed block take less space; possibly by aligning tiles that start and end with long ranges of the same bytes. The exact principles used for this optimisation are unknown, but are not as optimal as they could be; recompression after maximizing the longest repeating ranges gives results that are compressed better than what the original files have.
Tile map data
The tile map data contains the references needed to use the tiles to build up the final images. Each tile map has the same dimensions, namely, TilesWidth×TilesHeight tiles, using the values defined in the header. The final image, in pixels, will have one 8×8 tile for each position in the tile map, so its dimensions will be (TilesWidth * 8)×(TilesHeight * 8). A tile map is filled up with tiles until it is full, wrapping around to new lines when reaching the width. However, the actual compression seems to be strictly done per line, so the result of processing one block of data will never cause tiles to wrap around the end of a line onto the next one. If there is more data to process after that, more tile maps are filled, until all data is processed. In all of the original files, the tile map data ends exactly at the end of a completely filled tile map.
The data consists of UINT16LE blocks that either specify what the next tile should be and how many times to write it, or how to fill an entire line with data from a previously-assembled tile data line. Each tile is an 8x8 image taken from the decompressed image data, indicated by its Y-offset.
After reading one UINT16LE value, there are two cases: if the value is 0x0FFF (bytes FF 0F), it is a special line copy operation. Otherwise, it is a normal tile read.
Tile repeat block
On a normal tile read, the highest nibble in the read value is the amount of extra added tiles after writing the tile. So the actual numbers of tiles to write should be that amount + 1. As mentioned, the compression is done per line, so this repeat amount never crosses over into a next line or tile map.
The remainder, with the repeat value removed, should be multiplied by 4 to get the Y-offset of the requested tile in the decompressed image data. This means that to get the tile number inside the tile image data block, the value should be divided by 2. To get the actual offset in the image data, it should instead be multiplied by 16.
Using bit masking and bit shifting operations to get the data out, this becomes:
// Take highest nibble, shift it down to base position, then add 1.
int nrOfTiles = ((value & 0xF000) >> 12) + 1;
// Take lowest three nibbles, multiply by 16 by shifting up by 4 bits.
int offset = (value & 0x0FFF) << 4;
So, for example, the bytes 24 A1 will become value 0xA124, meaning, the amount of extra tiles to add is 0x0A, or 10 in decimal. The tile to use is the one at Y-offset 0x124 * 4 = 0x490, or 1168 in decimal, which is tile number is 0x124 / 2 = 0x92, or 146 in decimal, and its actual offset in the tile data is 0x124 * 0x10 = 0x1240, or 4672 in decimal.
The tile should be painted at the location of your current index, with the X and Y values multiplied by 8. So if your TilesWidth is 16, and you have written 18 tiles so far, that amount should be divided by the tiles width, to get a Y of 1 and a remainder of 2 to serve as X. So the new 8x8 tile should be painted on coordinates starting from Y= 1*8, X= 2*8.
Note that the source Y-offset is saved as a multiple of 4 lines rather than 8, meaning it is theoretically possible to read data halfway between two tiles, but in reality, none of the PAN files ever do that. This also means the values for normal tile reads are always even numbers.
Because of this, the largest value that can be stored in three nibbles is 0xFFE. Since this represents the maximum amount of 4-line skips, the actual amount of 8-line skips that can be addressed inside one file is half of that, so 0x7FF, or decimal 2047. So that is the maximum amount of tiles that can be used to build up the tile maps.
Line copy block
If the read value is 0x0FFF, then an additional value should be read. It will contain the instructions for copying a previously-constructed line. This value should only ever occur when starting a new line in the constructed tileset image. Note that if all tiles on a line are identical, and the width of a line is no more than 16 tiles, the line copy operation is not used, since it takes twice the amount of space of a simple single-tile repeat block.
The new value, again read as UINT16LE, contains the line number in the low byte, and the image index in the high byte. The line number is in tiles, so skip by 8 lines of actual pixels. The image index refers to the index in the list of constructed tileset images, but it can also be the current image, if the line number is lower than the currently processed line.
// Take lowest byte, multiply with width to get tile index. If the actual pixel offset is required, multiply this by 8.
int rowStart = (value & 0xFF) * TilesWidth;
// Take highest byte, shift it down to base position.
int imgIndex = (value & 0xFF00) >> 8;
So if you have bytes FF 0F 08 01, this is a line copy with a data value of 0x0108, meaning the image to copy from is image 01 (the second image), and the tiles line to copy from that image is line 08 (the 9th line).
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 | N/A | N/A | Optimises tile ordering to create better compression ranges, resulting in smaller sizes than the original game files. |
Credits
This file format was reverse engineered by Trogdor, with some deeper research and simplification of the logic by Nyerguds. 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!)