Files with WWD extension (Wap World Document) are designed to store level data in games based on the WAP32 engine. Depending on the actual game in order to edit these files you’d use: Gruntz Level Editor (GLE) in the game Gruntz or Wap World in the case of the game Claw. Implementation-wise these editors don’t differ much, only GLE incorporates a few additional new features. For this very reason this article will be dedicated to WWD files produced by GLE, which are technically a functional superset of WWD files produced by the Wap World editor.

Below is the formal WWD file specification, taking into account all the subtle differences between Gruntz and Claw level editors.

A few basic definitions are required to get things started:

  • plane – every level is comprised of planes. One of them is required to be marked as “main” – that’s where all the action takes place. Other planes are purely optional and are for decorational purposes only.
  • tiles – every plane is a rectangular board comprised of tiles.
  • tile properties – every tile in the main plane may have a different functionality. Tile properties describe how the tile interacts with other objects in its vicinity and what it’s responsible for.

1. Data types

There are the following different data types throughout this specification:

  • null-terminated string and fixed-length string – typical strings, really. The former is a series of character bytes terminated with the NULL character, while the latter has a fixed length.
  • null-terminated, fixed-length string – it’s an interesting hybrid of the two, which in my opinion is a result of a bug in the level editor implementation. Most of the strings in the editor correspond to a fixed number of bytes in a WWD file. For example the “Author” field has 64 bytes reserved. They’re not typical fixed-length strings however – if none of those 64 bytes is a NULL character, the editor will keep going and interpret the following bytes as a part of the same string. In practice then for the editor to interpret the string correctly, the “Author” field may contain only up to 63 characters while the last 64th byte should always be set to a NULL character.
  • uint32_t and int32_t – unsigned and signed four byte integer type. All integer values in the specification are little endian, i.e. the least significant bits are stored in the first byte of the number. An important remark: whether a number is signed or not was determined solely based on the way the editor interprets and renders the data. The game itself may theoretically interpret the data differently – keep that in mind.

In a few places there’s also a data type named rect, which is defined as follows:

struct rect
{
	int32_t left;
	int32_t top;
	int32_t right;
	int32_t bottom;
};

2. WWD header and the main block

The first 1524 bytes of data is the WWD file header. The rest of the file from now on will be referred to as the main block. Depending on the Compress flag in the World Properties of the editor, the main block will have a different structure:

  • if the flag is not set, the main block doesn’t require any preprocessing as is analyzed as-is.
  • if the flag is set, the entire main block is compressed with the deflate algorithm. In order to load the WWD file the main block has first to be decompressed. After that you treat the entire file as if it was never compressed to begin with.

All offsets in this specification have values relative to the very beginning of the file assuming the compression was not used.

The most important fields in the WWD header include:

  • the offset to the definition of planes
  • the offset to the tile properties
  • the size of the decompressed main block (only applicable when the Compress flag is set. Otherwise the field is zero)
  • the checksum, which is calculated based on the main block data. If it’s value is incorrect the editor and the game will reject the level and not attempt to load it.

All the subsequent sections of the file (plane headers, tiles, objects etc.) may be placed in the file in any order, since only the offset values are required to locate each section. Nevertheless the sections will be explained in this specification in the order they occur in WWD files generated by the editor.

struct wwd_header
{
	// 0x0 (0) - (?) WWD signature (and/or header size)
	uint32_t signature; // 0x000005F4

	// 0x4 (4)
	uint32_t unknown1; // 0x00000000

	// 0x8 (8) - flags
	// 0x1 - use z coords
	// 0x2 - compress
	uint32_t flags;

	// 0xC (12)
	uint32_t unknown2; // 0x00000000

	// 0x10 (16) - name
	// null terminated, fixed length string
	char name[64];

	// 0x50 (80) - author
	// null terminated, fixed length string
	char author[64];

	// 0x90 (144) - birth
	// null terminated, fixed length string
	char birth[64];

	// 0xD0 (208) - rez file path
	// null terminated, fixed length string
	char rez_file[256];

	// 0x1D0 (464) - image dir
	// null terminated, fixed length string
	char image_dir[128];

	// 0x250 (592) - pal rez
	// null terminated, fixed length string
	char pal_rez[128];

	// 0x2D0 (720) - start x
	int32_t start_x;

	// 0x2D4 (724) - start y
	int32_t start_y;

	// 0x2D8 (728)
	uint32_t unknown3; // 0x00000000

	// 0x2DC (732) - number of planes
	uint32_t num_planes;

	// 0x2E0 (736) - offset to the definition of planes
	uint32_t offset_planes;

	// 0x2E4 (740) - offset to the properties of tiles
	uint32_t offset_tile_properties;

	// 0x2E8 (744) - size of the decompressed main block
	// 0 for decompressed files
	uint32_t decompressed_mainblock_size;

	// 0x2EC (748) - checksum
	uint32_t checksum;

	// 0x2F0 (752)
	uint32_t unknown4; // 0x00000000

	// 0x2F4 (756) - launch app
	// null terminated, fixed length string
	char launch_app[128];

	// 0x374 (884) - image set 1
	// null terminated, fixed length string
	char image_set1[128];

	// 0x3F4 (1012) - image set 2
	// null terminated, fixed length string
	char image_set2[128];

	// 0x474 (1140) - image set 3
	// null terminated, fixed length string
	char image_set3[128];

	// 0x4F4 (1268) - image set 4
	// null terminated, fixed length string
	char image_set4[128];

	// 0x574 (1396) - prefix 1
	// null terminated, fixed length string
	char prefix1[32];

	// 0x594 (1428) - prefix 2
	// null terminated, fixed length string
	char prefix2[32];

	// 0x5B4 (1460) - prefix 3
	// null terminated, fixed length string
	char prefix3[32];

	// 0x5D4 (1492) - prefix 4
	// null terminated, fixed length string
	char prefix4[32];
}

The exact algorithm for calculating the checksum value is rather … unconventional. Below is its definition written in pseudo-C:

// the main block data in its final form as it is supposed to be saved to the file
// either compressed (when the flag Compress is set) or not
unsigned char* mainBlockData;
// the size of the mainBlockData buffer
unsigned int mainBlockSize;

// the calculated checksum
unsigned int checksum = -mainBlockSize;

for(unsigned int offset = 1; offset < mainBlockSize; ++offset)
	checksum += mainBlockData[offset] - offset;

// if the main block data is compressed
if( isCompressed )
{
	// decompressed main block data
	unsigned char* decompressedMainBlockData = ... ;
	checksum += decompressedMainBlockData[mainBlockSize];
}

The gist of the algorithm is that for every byte of the main block (in the exact shape that it’s written to the file, be it compressed or not) its value is added to the checksum while its offset (relative to the beginning of the main block) is subtracted from the checksum. There’s one caveat though: the calculation process starts with the second byte of the main block while the first byte is completely ignored. Further more, if the main block is compressed you also have to add in the value of the next byte of the uncompressed main block. It’s so complicated for a “reason” but I’ll reserve that topic for another article. A savvy programmer might already suspect what’s going on. All I will say at this point is that I believe that this algorithm is a result of a quite pesky programming error in the editor implementation.

In my tests I’ve discovered that the value of the checksum doesn’t have to be exactly the same as the editor calculates in order to be loaded properly. I haven’t yet reversed the exact algorithm for verifying the checksum, though once I do I will update this article. As far as I can tell you can safely omit the last part of the algorithm where you have to meddle with the uncompressed main block data and neither the editor nor the game will have any complaints.

3. Plane headers

wwd_header.offset_planes

At the very beginning of the main block GLE saves a sequence of headers of all available planes. The number of planes is saved in the WWD header (wwd_header.num_planes). Each header has 160 bytes.

The most important fields in a plane header are:

  • offset to the plane tiles
  • offset to the list of the plane image sets
  • offset to the list of objects on the plane (GLE saves the objects for the main plane only – for any other planes the offset is equal to 0)
struct plane_header
{
	// 0x0 (0) - (?) block size
	uint32_t block_size; // 0x000000A0

	// 0x4 (4)
	uint32_t unknown1; // 0x00000000

	// 0x8 (8) - flags
	// 0x01 - main plane
	// 0x02 - no draw
	// 0x04 - x wrapping
	// 0x08 - y wrapping
	// 0x10 - auto tile size
	uint32_t flags;

	// 0xC (12)
	uint32_t unknown2; // 0x00000000

	// 0x10 (16) - name
	// null terminated, fixed length string
	char name[64];

	// 0x50 (80) - width of the plane in px (tiles wide * tiles width in px)
	int32_t width_px;

	// 0x54 (84) - height of the plane in px (tiles high * tiles height in px)
	int32_t height_px;

	// 0x58 (88) - tiles width in px
	int32_t tiles_width;

	// 0x5C (92) - tiles height in px
	int32_t tiles_height;

	// 0x60 (96) - dimensions - tiles wide
	int32_t tiles_wide;

	// 0x64 (100) - dimensions - tiles high
	int32_t tiles_high;

	// 0x68 (104)
	uint32_t unknown3; // 0x00000000

	// 0x6C (108)
	uint32_t unknown4; // 0x00000000

	// 0x70 (112) - movement - x percent
	int32_t movement_x_percent;

	// 0x74 (116) - movement - y percent
	int32_t movement_y_percent;

	// 0x78 (120) - fill color
	int32_t fill_color;

	// 0x7C (124) - number of image sets
	uint32_t num_image_sets;

	// 0x80 (128) - number of objects
	uint32_t num_objects;

	// 0x84 (132) - offset to the tiles
	uint32_t offset_tiles;

	// 0x88 (136) - offset to the names of image sets
	uint32_t offset_image_sets;

	// 0x8C (140) - offset to objects
	uint32_t offset_objects;

	// 0x90 (144) - z coord
	int32_t z_coord;

	// 0x94 (148)
	uint32_t unknown5; // 0x00000000

	// 0x98 (152)
	uint32_t unknown6; // 0x00000000

	// 0x9C (156)
	uint32_t unknown7; // 0x00000000
}

4. Plane tiles

plane_header.offset_tiles

This section contains the list of tiles of which the plane is comprised. Each tile is represented by a 4-byte ID number (uint32_t). The number of tiles is: plane_header.tiles_wide * plane_header.tiles_high.

The tiles are indexed from the top-left to the bottom-right corner of the plane, row by row.

Instead of an ID number there may be one of two predefined values, which mean the following:

  • 0xFFFFFFFF – the tile is invisible.
  • 0xEEEEEEEE – the tile is filled.

5. Plane image sets

plane_header.offset_image_sets

In this section there is the list of image sets from which all available tiles are supplied. It’s a sequence of plane_header.num_image_sets null-terminated strings.

GLE for some reason ignores all image sets but the first one and so all tiles available in the editor are supplied only from the first image set on the list.

6. Plane objects

plane_header.offset_objects

This is a sequence of all objects on the plane. There is plane_header.num_objects of them and they are saved in order one after another.

One has to pay attention to the flag-based fields object_type and flags_hit_type. The flags user1 and user2 should not be used because of a nasty implementation bug in the editor. If you look closely at their bit masks in the listing below you will see that the mask of the flag user3 shares the bits with the masks of the user1 and user2 flags. The field object_type does not use any bit operations and uses mask values directly so using this field is relatively safe. The flags_hit_type field does however and because of that the flags user1 and user2 are not reliable and should not be used at all, or at least not in conjunction with the editor. I plan to make a blog article on that subject explaining it in more detail.

struct object
{
	// 0x0 (0) - id
	int32_t id;

	// 0x4 (4) - number of characters in the name string
	uint32_t size_name;

	// 0x8 (8) - number of characters in the logic string
	uint32_t size_logic;

	// 0xC (12) - number of characters in the image set string
	uint32_t size_image_set;

	// 0x10 (16) - number of characters in the animation string
	uint32_t size_animation;

	// 0x14 (20) - location x
	int32_t location_x;

	// 0x18 (24) - location y
	int32_t location_y;

	// 0x1C (28) - location z
	int32_t location_z;

	// 0x20 (32) - location_i
	int32_t location_i;

	// 0x24 (36) - flags - add flags
	// 0x01 - difficult
	// 0x02 - eye candy
	// 0x04 - high detail
	// 0x08 - multiplayer
	// 0x10 - extra memory
	// 0x20 - fast cpu
	uint32_t flags_add;

	// 0x28 (40) - flags - dynamic flags
	// 0x1 - no hit
	// 0x2 - always active
	// 0x4 - safe
	// 0x8 - auto hit damage
	uint32_t flags_dynamic;

	// 0x2C (44) - flags - draw flags
	// 0x1 - no draw
	// 0x2 - mirror
	// 0x4 - invert
	// 0x8 - flash
	uint32_t flags_draw;

	// 0x30 (48) - flags - user flags
	// 0x001 - flag 1
	// 0x002 - flag 2
	// 0x004 - flag 3
	// 0x008 - flag 4
	// 0x010 - flag 5
	// 0x020 - flag 6
	// 0x040 - flag 7
	// 0x080 - flag 8
	// 0x100 - flag 9
	// 0x200 - flag 10
	// 0x400 - flag 11
	// 0x800 - flag 12
	uint32_t flags_user;

	// 0x34 (52) - score
	int32_t score;

	// 0x38 (56) - points
	int32_t points;

	// 0x3C (60) - powerup
	int32_t powerup;

	// 0x40 (64) - damage
	int32_t damage;

	// 0x44 (68) - smarts
	int32_t smarts;

	// 0x48 (72) - health
	int32_t health;

	// 0x4C (76) - rect move
	rect rect_move;

	// 0x5C (92) - rect hit
	rect rect_hit;

	// 0x6C (108) - rect attack
	rect rect_attack;

	// 0x7C (124) - rect clip
	rect rect_clip;

	// 0x8C (140) - rect user1
	rect rect_user1;

	// 0x9C (156) - rect user2
	rect rect_user2;

	// 0xAC (172) - user1
	int32_t user1;

	// 0xB0 (176) - user2
	int32_t user2;

	// 0xB4 (180) - user3
	int32_t user3;

	// 0xB8 (184) - user4
	int32_t user4;

	// 0xBC (188) - user5
	int32_t user5;

	// 0xC0 (192) - user6
	int32_t user6;

	// 0xC4 (196) - user7
	int32_t user7;

	// 0xC8 (200) - user8
	int32_t user8;

	// 0xCC (204) - min x
	int32_t min_x;

	// 0xD0 (208) - min y
	int32_t min_y;

	// 0xD4 (212) - max x
	int32_t max_x;

	// 0xD8 (216) - max y
	int32_t max_y;

	// 0xDC (220) - speed x
	int32_t speed_x;

	// 0xE0 (224) - speed y
	int32_t speed_y;

	// 0xE4 (228) - tweak x
	int32_t tweak_x;

	// 0xE8 (232) - tweak y
	int32_t tweak_y;

	// 0xEC (236) - counter
	int32_t counter;

	// 0xF0 (240) - speed
	int32_t speed;

	// 0xF4 (244) - width
	int32_t width;

	// 0xF8 (248) - height
	int32_t height;

	// 0xFC (252) - direction
	int32_t direction;

	// 0x100 (256) - face dir
	int32_t face_dir;

	// 0x104 (260) - time delay
	int32_t time_delay;

	// 0x108 (264) - frame delay
	int32_t frame_delay;

	// 0x10C (268) - hits - object type (single value)
	// 0x001 - generic
	// 0x002 - player
	// 0x004 - enemy
	// 0x008 - powerup
	// 0x010 - shot
	// 0x020 - p-shot
	// 0x040 - e-shot
	// 0x080 - special
	// 0x100 - user1 (usage not recommended)
	// 0x200 - user2 (usage not recommended)
	// 0x300 - user3 (yes - the value is not a mistake)
	// 0x400 - user4 (yes - the value is not a mistake)
	uint32_t object_type;

	// 0x110 (272) - hits - hit type flags
	// 0x001 - generic
	// 0x002 - player
	// 0x004 - enemy
	// 0x008 - powerup
	// 0x010 - shot
	// 0x020 - p-shot
	// 0x040 - e-shot
	// 0x080 - special
	// 0x100 - user1 (should be deprecated)
	// 0x200 - user2 (should be deprecated)
	// 0x300 - user3 (yes - the value is not a mistake)
	// 0x400 - user4 (yes - the value is not a mistake)
	// 0xFFFFFFFF - all (separate, special value overriding all other flags)
	uint32_t flags_hit_type;

	// 0x114 (276) - move res x
	uint32_t move_res_x;

	// 0x118 (280) - move res y
	uint32_t move_res_y;

	// 0x11C (284) - name string
	// fixed length, object.size_name characters
	char* name;

	// (variable) - logic string
	// fixed length, object.size_logic characters
	char* logic;

	// (variable) - image set string
	// fixed length, object.size_image_set characters
	char* image_set;

	// (variable) - animation string
	// fixed length, object.size_animation characters
	char* animation;
}

7. Tile properties

wwd_header.offset_tile_properties

The first 32 bytes is the header of the section:

struct tile_properties_header
{
	// 0x0 (0)
	uint32_t unknown1; // 0x00000020

	// 0x4 (4)
	uint32_t unknown2; // 0x00000000

	// 0x8 (8) - the number of the tile properties
	uint32_t num_tile_properties;

	// 0xC (12)
	uint32_t unknown3; // 0x00000000

	// 0x10 (16)
	uint32_t unknown4; // 0x00000000

	// 0x14 (20)
	uint32_t unknown5; // 0x00000000

	// 0x18 (24)
	uint32_t unknown6; // 0x00000000

	// 0x1C (28)
	uint32_t unknown7; // 0x00000000
}

After the header there is a contiguous list of all tile properties (there is tile_properties_header.num_tile_properties of them). The tile ID a property refers to depends on its index in the list. The indices are zero-based and as such the first property in the list refers to the tile with ID 0, the second one is tile ID 1 and so on. It might be quite a surprise for some since all the tiles defined in Gruntz (and Claw to my knowledge) have IDs starting with 1 and not 0. It’s also worth noting that since the list is contiguous there may be properties referring to non-existing tiles – they simply serve as data fillers and are not used by the editor for any purpose.

Each tile property starts with the following 16 bytes:

struct tile_property_base
{
	// 0x0 (0) - tile type
	// 0x1 - single
	// 0x2 - double
	// 0x3 - mask
	uint32_t tile_type;

	// 0x4 (4)
	uint32_t unknown1; // 0x00000000

	// 0x8 (8) - tile width
	uint32_t width;

	// 0xC (12) - tile height
	uint32_t height;
}

The following bytes depend on the value of the field tile_property_base.tile_type, which may take one of three values: 1 (single), 2 (double) or 3 (mask).

  • single (1) – the most basic type of a tile – it makes the tile accept a single attribute. All the tiles in Gruntz have this type.
  • double (2) – the tile has a rectangular area defined which accepts one attribute (an “inside” attribute). The remainder of the area may have a different attribute (an “outside” attribute). This tile type is used in Claw and makes the playable character be able to interact with different parts of tiles in a different way (for example make only a part of a tile solid).
  • mask (3) – each pixel of the tile may have a separate attribute, which are saved as a mask. Each pixel value of the mask represent a single attribute of a corresponding pixel of the tile. This tile type is not used by any of the games. Although GLE has support for mask tile types it may act funky and in the worst case even make the editor crash.

A few words on the attributes – an attribute is a numeric value which represents the way the tile (or its part) is interacting with the rest of the game. WapWorld and GLE define five main attributes: Clear, Solid, Ground, Climb and Death. Gruntz and GLE expand this list further to almost 60 values – each attribute other than the first five main ones is treated as a User Attribute.

Here are the complete definitions of each type of tile properties and the values for the main attributes:

// Standard attribute values in WapWorld and GLE:
// 0x00 (0) - clear
// 0x01 (1) - solid
// 0x02 (2) - ground
// 0x03 (3) - climb
// 0x04 (4) - death

struct tile_property_single
{
	tile_property_base base;

	// 0x10 (16) - tile attribute
	int32_t attribute;
}

struct tile_property_double
{
	tile_property_base base;

	// 0x10 (16) - outside tile attribute
	int32_t attribute_outside;

	// 0x14 (20) - inside tile attribute
	int32_t attribute_inside;

	// 0x18 (24) - tile rect
	rect rect;
}

struct tile_property_mask
{
	tile_property_base base;

	// 0x10 (16) - mask data
	// tile_property_base.width * tile_property_base.height bytes
	char* mask;
}