Sauerbraten map format documentation

Feel free to use this documentation in any way whatsoever. No permission, attribution or payment necessary.

Projects

[2020-08-10] Salatiel has made a web tool to create Sauerbraten maps from JSON data: https://salatielsauer.github.io/OGZ-Editor/.
[2015-02-11] Benjamin Summers is working on a Haskell library to read, write, and generate Sauerbraten maps: https://github.com/bsummer4/ogz.
Are you working on a project using this documentation? Get in touch - james@incoherency.co.uk - and I'll mention it here.

Introduction

Firstly, this documentation is for the version 29 map format, and it may change in the future.
The map file is gzip'd, but as this is fairly trivial to do, I shall describe the non-compressed part.
Note that all integer values are stored in little-endian.
The maps are organised in to several sections, as follows, which are concatenated together and then gzip'd to produce an OGZ map file.

Header

The header is a 36-byte structure containing some useful information about the map. Here is the structure definition from src/engine/world.h:
struct octaheader
{
    char magic[4];              // "OCTA"
    int version;                // any >8bit quantity is little endian
    int headersize;             // sizeof(header)
    int worldsize;
    int numents;
    int numpvs;
    int lightmaps;
    int blendmap;
    int numvars;
};
This structure is written straight into the first 36 bytes of the uncompressed map file. The magic field must be the 4-byte string (not zero-terminated) "OCTA" in order for the map to load correctly. For the format described herein, version must be 29, and headersize must be 36. The worldsize is the length of one side of the cube that contains the map (so that we know how big each cube of the octree is). If worldsize is not a power of 2, you will get some very strange effects. Numents is just the number of entities contained in the Entities section. Numpvs, lightmaps, and blendmap are not discussed in this documentation, so I just set them all to 0 and forget about them (you can run calclight from within edit mode to get these filled in properly). Numvars is the number of variables in the Variables section.

Example:
magicversionheadersizeworldsizenumentsnumpvslightmapsblendmapnumvars
OCTA2936102410001
4f 43 54 411d 00 00 0024 00 00 0000 04 00 0001 00 00 0000 00 00 0000 00 00 0000 00 00 0001 00 00 00

Variables

This section is a series of map variables. There are 3 basic types that can come in this section: All variables contain a single byte to specify the type (either 0, 1, or 2, as above), followed by a 2-byte value (of type unsigned short) to specify the length of the variable name, followed by the variable name (not zero-terminated), finally followed by the value of the variable. For an int variable, this is a 4-byte value containing the int, for a float it is a 4-byte value containing the float, and for a string it is a 2-byte unsigned short followed by the non-zero-terminated string.

Example:
typenamevalue
SVAR6skybox15ik2k/env/iklake
0206 0073 6b 79 62 6f 780f 0069 6b 32 6b 2f 65 6e 76 2f 69 6b 6c 61 6b 65

Gameident

This is a string to identify which game the map is intended for. It consists of a single byte to give the length of the string, followed by a string of that length, plus a zero-terminating byte. For normal Sauerbraten, this string is just "fps".

Example:
lengthstring
3fps
0366 70 73 00

Some information about 'extras'

I have no idea what this section is for, but what I gather from the Sauerbraten code is that there is an unsigned short value "extraentinfosize", followed by an unsigned short "extras.length". I don't know under what circumstances these could be non-zero (perhaps they're legacy?), so I just leave them as 0.

Example:
extraentinfosizeextras.length
00
00 0000 00

Texture MRU

This is the contents of the Most-Recently-Used cache of texture indices. It conssists of an unsigned short quantity "texmru.length", which specifies the number of entries in the MRU, followed by that many unsigned short values which are the values in the cache. I don't know if this information is useful for anything other than getting recently-used textures to appear first when a mapper scrolls through the textures. I set texmru.length to 0 (followed by a 0-length array of texture indices, i.e. no array) and everything appears to work fine. If you want, you can fill it with texture indices (like I have in the example below).

Example:
texmru.lengthtexmru
52, 4, 3, 5, 7
05 0002 00 04 00 03 00 05 00 07 00

Entities

This section consists of a number of entities concatenated together. The number of entities present is given in the "numents" field of the header. Here is the structure definition from src/shared/ents.h:
enum { ET_EMPTY=0, ET_LIGHT, ET_MAPMODEL, ET_PLAYERSTART, ET_ENVMAP, ET_PARTICLES, ET_SOUND, ET_SPOTLIGHT, ET_GAMESPECIFIC };

struct entity                                   // persistent map entity
{
    vec o;                                      // position
    short attr1, attr2, attr3, attr4, attr5;
    uchar type;                                 // type is one of the above
    uchar reserved;
};
The "o" field is of a structure containing three 4-byte floats to specify the x, y, and z co-ordinates (in that order), and the "type" field is as in the following table. The Sauerbraten editing reference can give a lot more information for the meaning of the attribute values.
typenameattributes
0ET_EMPTY
1ET_LIGHT
2ET_MAPMODELattr1 specifies the angle in degrees in the XY plane that the model is rotated by. attr2 specifies the model number. attr3 and attr4 control trigger behaviour.
3ET_PLAYERSTARTattr1 gives the angle in degrees in the XY plane that the player is looking when he spawns. attr2 specifies the team for which this is a spawn point in team modes, either 1 or 2 (or 0 if it is for non-team modes).
4ET_ENVMAPattr1 specifies the radius of the environment map.
5ET_PARTICLES
6ET_SOUNDattr1 specifies the sound index. attr2 specifies the radius in which a player must be in order to hear the sound. attr3, if non-zero, specifies the size within which the volume is maximal, and will start tapering when outside this size.
7ET_SPOTLIGHT
8ET_GAMESPECIFICGame specific; unused in Sauerbraten


Example:
positionattributestypereserved
972.0, 972.0, 516.0336, 0, 0, 0, 0ET_PLAYERSTART0
00 00 73 44 00 00 73 44 00 00 01 4450 01 00 00 00 00 00 00 00 000300

Map geometry (octree)

The octree is stored in the file by recursing through the octree and writing out data for each cube as it is encountered. An important thing to note is that there is an implicit cube-split. That is, there must be data in the map file for 8 cubes, rather than just 1, because when loading the map, Sauerbraten generates a root cube, and then loads it's 8 children from the map file. The bottom 4 cubes come first, and then the top 4. There are 5 types of cube that can be present in this section, as in the table below. Each cube is stored in the file as a single byte to identify the type of cube, followed by 6 unsigned short's to specify texture indices (for all cubes except OCTSAV_CHILDREN), followed by a single-byte mask. The mask is described in more detail below. Finally, if the cube is OCTSAV_NORMAL, 12 bytes describing the 24 edge deformations are stored. According to the comments, there are 24 4-bit values which "denote the range".
typenamedatadescription
0OCTSAV_CHILDRENNo more data.Indicates that instead of moving on to the next child for the current parent cube, the map loader should recurse down and load 8 children for this cube.
1OCTSAV_EMPTY6 textures (though they are never visible) and a mask.Indicates a cube of empty space.
2OCTSAV_SOLID6 textures and a mask.A cube of solid space.
3OCTSAV_NORMAL6 textures, a mask, and 24 edges.TODO: Find out about this. This is basically a cube that has been deformed by moving its corners.
4OCTSAV_LODCUBE6 textures and a mask.This is the same as OCTSAV_CHILDREN, except it has its own textures, which presumably means that Sauerbraten will draw this instead of child cubes when the viewer is far enough away.
The mask is used to set extra properties of the cube. The most useful thing to do is to set the high bit to 1. If the high bit of the mask is set, it indicates that the byte immediately after the mask describes the material that the "empty" space should be made of, and some other properties, as follows:
bitsmaterial/properties
......00Air
......01Water
......10Lava
......11Glass
...01...No clip (no collision)
...10...Clip (full cube collision)
...11...Game-specific clip
..1.....Death
1.......Edit-only surface
If the lower 6 bits of the mask are set, then Sauerbraten does some unintelligible business with surfaces and lighting. If this is important to you, examine src/engine/worldio.cpp, from around line 201.
In the example below, a minimal world consisting of 8 cubes is produced.

Example:
typetexturesmask
OCTSAV_SOLID2, 3, 4, 5, 6, 70
0202 00 03 00 04 00 05 00 06 00 07 0000
OCTSAV_SOLID2, 3, 4, 5, 6, 70
0202 00 03 00 04 00 05 00 06 00 07 0000
OCTSAV_SOLID2, 3, 4, 5, 6, 70
0202 00 03 00 04 00 05 00 06 00 07 0000
OCTSAV_SOLID2, 3, 4, 5, 6, 70
0202 00 03 00 04 00 05 00 06 00 07 0000
OCTSAV_EMPTY0, 0, 0, 0, 0, 00
0100 00 00 00 00 00 00 00 00 00 00 0000
OCTSAV_EMPTY0, 0, 0, 0, 0, 00
0100 00 00 00 00 00 00 00 00 00 00 0000
OCTSAV_EMPTY0, 0, 0, 0, 0, 00
0100 00 00 00 00 00 00 00 00 00 00 0000
OCTSAV_EMPTY0, 0, 0, 0, 0, 00
0100 00 00 00 00 00 00 00 00 00 00 0000

Lightmaps, PVS, blendmap

This section contains some information (which I personally don't need) presumably about lightmaps, the PVS, and something about a blend map. If lightmaps, numpvs, and blendmap are all set to 0 in the header, then this section is not present (i.e. has length 0).

Finished map file

If we now concatenate all of the sections together, we end up with the following map data:

sectiondata
header4f 43 54 41 1d 00 00 00 24 00 00 00 00 04 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00
variables02 06 00 73 6b 79 62 6f 78 0f 00 69 6b 32 6b 2f 65 6e 76 2f 69 6b 6c 61 6b 65
gameident03 66 70 73 00
extras information00 00 00 00
texture mru05 00 02 00 04 00 03 00 05 00 07 00
entities00 00 73 44 00 00 73 44 00 00 01 44 50 01 00 00 00 00 00 00 00 00 03 00
octree02 02 00 03 00 04 00 05 00 06 00 07 00 00 02 02 00 03 00 04 00 05 00 06 00 07 00 00 02 02 00 03 00 04 00 05 00 06 00 07 00 00 02 02 00 03 00 04 00 05 00 06 00 07 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00
lightmaps, pvs, blendmap
which you can download as example.oct, and gzip to get a finished map like example.ogz. This will be an empty map (more or less the same as what you get with /newmap), but because of the ET_PLAYERSTART the player will start in a corner of the map.