Tito Dal Canton

Physics is reverse engineering

Marathon shapes file format

Welcome. Here you will find a detailed description of Marathon shapes files: what they contain, how their content is packed and how to retrieve every piece of information. This document refers to files belonging to Marathon II: Durandal, Marathon Infinity and AlephOne, the open source evolution of the Marathon II engine. It doesn't currently cover the original Marathon shapes file format, which is similar but based on resource forks and thus is strictly bound to the MacOS architecture. All this information comes from my personal Marathon experience, a document found in the AlephOne engine web site, talks with the AlephOne crew, the original Marathon II sources and a bit of experimentation while writing the ShapeFusion editor. There may be errors and inaccuracies; reports are welcome!

Introduction to shapes files

Basic Marathon II scenarios are made of several files: a map file, a sounds file, an images file and a shapes file, which we are going to see. The shapes file contains

The shapes file does not contain

The shapes file is organized in collections. Each collection groups data belonging to a certain piece of the game; for example, there is a collection for each monster type, one for each texture set, one for each landscape and so on. Each collection is able to store two versions of the same data: one to be used when the game is run in 8-bit color displays, and one to be used when the game is experienced on modern true-color displays (this includes the AlephOne OpenGL mode). Note that the true-color version is rarely specified; it's mainly used where its effects are really noticeable: landscapes and weapons in hand. Also note that the true-color data does not contain true-color graphics! It's just 8-bit graphics like the other version, but with specialized color tables and usually improved pixel resolution.

I like to see collections as folders; each folder contains another folder for 8-bit data and, optionally, a folder for true-color data. Maybe this analogy is useful for you too.

Each collection version stores color tables, bitmaps, frames and sequences.

Having different color tables is an easy way to change the color of all animation sprites on the fly.

Since Marathon was originally written as a Mac application, shapes files are stored in big-endian byte order. As explained in the following sections, data structures contain a lot of unused space that was originally used at runtime to store pointers and other info.

The collection headers

The shapes file begins with 32 collection headers. A collection header is 32 bytes long and has the following structure:

shortstatus
unsigned shortflags
longoffset8
longlength8
longoffset16
longlength16
unsigned charunused[12]

status and flags seem to be unused and always set to 0. offset8 and length8 specify the position and size of the 8-bit collection version data block within the shapes file. Of course, offset16 and length16 do the same for the true-color collection version. Offsets are relative to the beginning of the file, and may be set to -1 to indicate that the correponding version is not present.

The collection definition

If you follow one of the offsets in the collection header, you find the corresponding collection version data block. This begins with a 544-byte collection definition data structure:

shortversion
shorttype
unsigned shortflags
shortcolors_per_table
shortcolor_table_count
longcolor_tables_offset
shorthigh_level_shape_count
longhigh_level_shape_table_offset
shortlow_level_shape_count
longlow_level_shape_table_offset
shortbitmap_count
longbitmap_table_offset
shortscale_factor
longcollection_size
unsigned charunused[506]

version gives the file format version (3 for the format described in this document). type tells what kind of data is contained in this collection. flags should be unused and is usually 0, but I've found cases with 1 instead (Rubicon shapes). I don't yet know what this means. scale_factor is meant to be the "pixels to world" conversion factor for the whole collection, but seems ignored by the AlephOne engine and probably has no effect. collection_size tells the total size of this collection version; it must be equal to the corresponding length8 or length16 field in the collection header. Other fields should be self-explanatory; table offsets point to arrays of longs specifying offsets to bitmaps, frames and sequences. Important: all these offsets are relative to the collection definition, not to the beginning of the file.

Color tables

The color_tables_offset field of the collection definition points to an array of color_table_count color tables, which are just arrays of colors_per_table 8-byte long RGB color value structures:

unsigned charflags
unsigned charvalue
unsigned shortred
unsigned shortgreen
unsigned shortblue

In other words, color_table_offset points to a big array of colors_per_table·color_table_count RGB color value structures. value is the color index used in bitmaps. red, green, blue are self-explanatory, but note that they are 16-bit colors, while usually you deal with 8-bit RGB values. Just shift right by 8 bits if this is the case. Finally, the most significant bit of flags is the self luminescent color flag which, if set, alters the shading properties of that color.

Bitmaps

The bitmap_table_offset field of the collection definition points to an array of bitmap_count longs, which are offsets to as many bitmap definition objects. Each bitmap definition is 30 bytes long and has the following structure:

shortwidth
shortheight
shortbytes_per_row
shortflags
shortbit_depth
unsigned charunused[20]

width and height are the pixel dimensions of the bitmap. bytes_per_row is -1 for compressed bitmaps, a positive value for uncompressed ones. Compression doesn't actually compress globally the pixel block, it just removes certain transparent areas, and so makes sense just for bitmaps with transparency. Bit 7 of flags is the column order flag: if enabled, pixels are stored in column-order rather than row-order (that is, first comes the first pixel column, then the second and so on). In that case bytes_per_row actually means bytes per column. Bit 6 of flags is the transparency enabled flag: if enabled, pixels set to color index 0 will be rendered as completely transparent. In shapes color tables, color 0 is traditionally set to bright blue (RGB 0, 0, 0xffff). Finally, bit_depth should always be set to 8.

After this preamble there is a certain amount of unused space to skip before coming to the actual pixel data. This space is 4·width bytes for column order bitmaps and 4·height bytes for row order bitmaps. This place stored pointers to scanlines in the original Mac engine, but has no meaning in files.

Then comes the actual pixel data. For uncompressed bitmaps it is just a block of width·height bytes, following the order specified by the column order flag. For compressed bitmaps one can't tell a priori how many bytes to read, they must be decoded. Decoding works like that: for each pixel column read two shorts, first_row and last_row. Then read last_row-first_row bytes and copy them as pixel values to the destination bitmap, starting from pixel row first_row. Proceed in the same way for every column and the bitmap is decoded. Encoding works the opposite way: for each column of the source bitmap, find the first opaque pixel and write its row, find the last and write its row incremented by 1, then copy all pixels of the column spanning between those two rows. As you see it's a very poor algorithm, efficient only for a limited set of bitmaps.

Frames

The low_level_shape_table_offset field of the collection definition points to an array of low_level_shape_count longs, which are offsets to as many low level shape definition objects. Each of these is 36 bytes long and has the following structure:

unsigned shortflags
longminimum_light_intensity
shortbitmap_index
shortorigin_x
shortorigin_y
shortkey_x
shortkey_y
shortworld_left
shortworld_right
shortworld_top
shortworld_bottom
shortworld_x0
shortworld_y0
unsigned charunused[8]

Bit 7 of flags is the X mirror flag, bit 6 the Y mirror flag and bit 5 the keypoint obscured flag. X and Y mirror flags, if set, cause the associated frame bitmap to be rendered flipped along the vertical and horizontal axis respectively. The keypoint flag makes sense only for the player collection, and controls wether the torso is drawn over the legs or the opposite.

minimum_light_intensity is a fixed point value ranging between 0 and 1 (0x10000) and specifies the minimum light intensity to use when rendering the bitmap. This is useful for creating self-luminescent sprites like flames and the hunter bolt.

bitmap_index is the associated bitmap index. -1 means the frame has no associated bitmap and thus is not valid.

origin_x and origin_y tell where the "logical" bitmap origin lies within the physical bitmap dimensions. Note that these fields are not used by the engine, they look like a temporary place for editors to calculate world_* fields.

key_x and key_y specify the position of the keypoint within the physical bitmap dimensions. The keypoint is used only for player collections and tells where to attach torso frames to leg frames: the torso origin is placed at the legs keypoint position. Note that these fields are not used by the engine, they look like a temporary place for editors to calculate world_* fields.

world_* fields encode the same info carried by the previous four fields. They represent the scaled bitmap rectangle and keypoint position in world coordinates. They are pre-computed by editors as follows:

where width and height are the associated bitmap dimensions and scale_factor comes from the sequence using this frame. When that value is set to 0, the scale_factor in the collection definition is used instead.

Sequences

The high_level_shape_table_offset field of the collection definition points to an array of high_level_shape_count longs, which are offsets to as many high level shape definition objects. Each of these is 88 bytes long and follows this structure:

shorttype
unsigned shortflags
charname[34]
shortnumber_of_views
shortframes_per_view
shortticks_per_frame
shortkey_frame
shorttransfer_mode
shorttransfer_mode_period
shortfirst_frame_sound
shortkey_frame_sound
shortlast_frame_sound
shortscale_factor
shortloop_frame
unsigned charunused[28]

type and flags should always be 0, but Rubicon has sequences with flags set to 1. I don't know what that means. name is a Pascal string that can be used to give a meaningful name to the sequence. Its first byte is the string length, followed by string chars. number_of_views specifies the number of angles the sequence can be viewed from, but it's not directly this information (the field name is quite misleading). Instead, the following table must be used:

number_of_views Animation type Actual number of views
10unanimated1
1animated11
3animated3to44 (0°, ±90°, 180°)
4animated44 (0°, ±90°, 180°)
9animated3to55
11animated55
2animated2to88 (0°, ±45°, ±90°, ±135°, 180°)
5animated5to88 (0°, ±45°, ±90°, ±135°, 180°)
8animated88 (0°, ±45°, ±90°, ±135°, 180°)

frames_per_view tells how many frames are used in each view. ticks_per_frame specifies the duration of the single frame in units of ticks. first_frame_sound, key_frame_sound and last_frame_sound tell which sound to play at the first, key and end frames respectively (sounds are taken from the associated scenario sounds file). scale_factor is meant to be the pixels-to-world scale factor for the sequence, but seems ignored by the AlephOne engine, and should have no effect. Referenced frames already carry all the necessary info about unit conversions.

After each high level shape definition block there is an array of signed short frame indexes, defining animation frames. You get the number of indexes as (number of views) times frames_per_view. Again, the number of views is not simply number_of_views but must be looked up in the table given before.

Last update: 2007-02-02