# BAR Map Technical Specification

Revision: 001  
Author: Methodox Technologies, Inc.

*A reference for the file formats and conventions behind a Beyond All Reason map.*

This document describes **what a map is**, not how to make one. The focus here is about the bytes on disk and the meaning the engine assigns to them, so that somebody writing their own compiler — or reading a map somebody else compiled — has one place to check.

BAR runs on **Recoil**, a fork of the Spring RTS engine, and its map format is Spring's `SM2`. Where this document and the engine disagree, the engine is right: the authority is `rts/Map/` in the `Recoil` source, and the notes below cite it where a detail is easy to get wrong.

## 1. Units and coordinates

Everything in a map is measured in **elmos**, Spring's world unit.

| Unit | Size | What uses it |
| --- | --- | --- |
| **Elmo** | — | Positions, heights, radii |
| **Square** | 8 elmos | The heightmap grid |
| **Texel** | 1 elmo | The ground texture, at 8 texels per square |
| **Map unit** | 512 elmos = 64 squares | How map size is quoted: an "8×8 map" |

Axes: **X and Z are the ground plane, Y is up.** The origin is a corner, not the centre, so X and Z run from 0 to the map's full extent and are never negative. A map declared as `mapx × mapy` squares spans `mapx × 8` elmos in X and `mapy × 8` elmos in Z.

Every grid in the format is stored **row-major with Z as the row index** — element `(x, z)` is at `z × width + x`. Viewed from above with X to the right, that puts the origin at the top-left, which is why map tools speak of it as the north-west corner. The engine itself has no compass; it only has this ordering.

`mapx` and `mapy` must each be a multiple of **128 squares** — that is, an even number of map units.

**Sea level is y = 0**, always. It is not a setting. A map has water where its terrain is below zero, and the way to have more sea is to put more terrain under zero.

## 2. The archive

A map is one archive containing everything it needs. Three containers are accepted:

| Extension | Container |
| --- | --- |
| `.sd7` | 7-Zip |
| `.sdz` | Zip |
| `.sdd` | A plain directory |

The layout inside:

```
mapinfo.lua                  the map's declaration — required
maps/<name>.smf              terrain, and pointers to everything else — required
maps/<name>.smt              the ground texture, as tiles — required
mapconfig/                   optional per-map overrides
maphelper/                   optional helper Lua
LuaGaia/  LuaRules/          optional gadgets
objects3d/  unittextures/    models and textures for map features
features/                    feature definitions
```

Only the first three are required. A map with just those loads and plays.

The `.smf` names its `.smt` by file name, and the engine resolves that name inside the same archive, so the two must agree.

## 3. `mapinfo.lua`

A Lua file returning one table. It carries everything the `.smf` does not: the map's name, its water, its lighting, its start positions and what its terrain types mean.

The keys that matter most:

```lua
local mapinfo = {
  name        = "My Map",
  shortname   = "My Map",
  description = "...",
  author      = "...",
  version     = "1.0",
  mapfile     = "maps/my_map.smf",   -- must match the archive path

  maphardness     = 100,     -- how hard the ground is to crater
  extractorRadius = 90,      -- how far a metal extractor reaches. Engine default 500
  gravity         = 130,     -- engine default 130
  tidalStrength   = 18,      -- passive energy from tidal generators. Default 0
  maxMetal        = 1.0,     -- §5 — scales the whole metal map. Default 0.02
  voidWater       = false,   -- true: no sea bed, just void below y=0

  smf = {
    minheight = -160,        -- optional override of the .smf header
    maxheight =  560,
  },

  water = { ... },           -- §7
  lighting = { ... },
  atmosphere = { ... },

  teams = {
    [0] = {startPos = {x = 1024, z = 1024}},
    [1] = {startPos = {x = 7168, z = 7168}},
  },

  terrainTypes = { ... },    -- §6
}
```

**Start positions live here, not in the `.smf`.** They are indexed from zero, in elmos, and the count determines how many players the map supports.

**Key case does not matter.** The engine's Lua parser lowercases every key in the table it loads, and lowercases the name it looks up as well (`LuaParser.cpp`, `lowerKeys` and `lowerCppKeys`, both default true). `extractorRadius`, `extractorRadius` and `ExtractorRadius` are the same key. Maps in the wild use all three.

Several defaults are worth knowing because they are nothing like a sensible map value — `extractorRadius` defaults to **500** and `maxMetal` to **0.02**. A map that omits them does not get a mild version of the intended behaviour; it gets something unplayable. Set them explicitly.

Most maps end with a standard postprocessing tail that merges optional overrides from `mapconfig/mapinfo/*.lua` into the table before returning it. It is a convention rather than a requirement, but tooling expects it.

## 4. The `.smf` — Spring map file

All integers are **little-endian**. All offsets are **absolute from the start of the file**.

### 4.1 Header — 80 bytes

| Offset | Type | Field | Notes |
| --- | --- | --- | --- |
| 0 | `char[16]` | magic | `"spring map file"` + NUL |
| 16 | `int32` | version | `1` |
| 20 | `int32` | mapid | Any non-zero value; identifies the map |
| 24 | `int32` | mapx | Width in squares. Multiple of 128 |
| 28 | `int32` | mapy | Depth in squares. Multiple of 128 |
| 32 | `int32` | squareSize | **8** |
| 36 | `int32` | texelsPerSquare | **8** |
| 40 | `int32` | tileSize | **32** |
| 44 | `float` | minHeight | World height that heightmap value 0 means |
| 48 | `float` | maxHeight | World height that heightmap value 65535 means |
| 52 | `int32` | heightmapPtr | |
| 56 | `int32` | typeMapPtr | |
| 60 | `int32` | tilesPtr | |
| 64 | `int32` | minimapPtr | |
| 68 | `int32` | metalmapPtr | |
| 72 | `int32` | featurePtr | |
| 76 | `int32` | numExtraHeaders | |

The three size fields are fixed in practice. The engine reads them, but the rest of the format — and every tool that consumes it — assumes 8, 8 and 32.

**`minHeight` and `maxHeight` are the whole vertical scale.** The heightmap stores 16-bit fractions of that range, so widening it coarsens every height in the map. A range of 720 elmos over 65,536 steps gives about 0.011 elmos of precision, which is far finer than anything visible; a range of 20,000 would not be.

### 4.2 Extra headers

`numExtraHeaders` records follow the header immediately. Each begins with:

| Type | Field |
| --- | --- |
| `int32` | size — the whole record's byte count |
| `int32` | type |

The only type in common use is **type 1, `MEH_Vegetation`**, whose record is 12 bytes: size, type, and an `int32` offset pointing at the grass map (§4.7). A map with no grass omits the record and sets `numExtraHeaders` to 0.

An unknown type is skipped using its `size`, so a reader must trust that field rather than assume.

### 4.3 Heightmap

`uint16[(mapx + 1) × (mapy + 1)]`, row-major, north-west first.

**Vertices, not cells** — hence the `+ 1` on each axis. Each value maps linearly onto
`minHeight … maxHeight`:

```
height_elmos = minHeight + value / 65535 × (maxHeight − minHeight)
value        = round((height_elmos − minHeight) / (maxHeight − minHeight) × 65535)
```

Sixteen bits is not optional in practice. Terrain quantised to 8 bits and widened afterwards terraces visibly on any slope gentle enough to matter, and the lost precision cannot be recovered.

### 4.4 Type map

`uint8[(mapx / 2) × (mapy / 2)]` — one byte per **2×2 squares**, that is per 16×16 elmos.

Each byte is an index into `terrainTypes` in `mapinfo.lua` (§6). It decides ground hardness, per-unit movement speed and whether tracks are left, and it is invisible: it does not affect what the ground looks like.

### 4.5 Minimap

A **1024×1024 DXT1** image with **9 mip levels** (1024 down to 4), stored largest first, totalling exactly **699,048 bytes**. The size is fixed regardless of the map's dimensions, so a non-square map's minimap is stretched to fit.

### 4.6 Metal map

`uint8[(mapx / 2) × (mapy / 2)]` — the same resolution as the type map, one byte per 16×16 elmos.

See §5 for what the byte means.

### 4.7 Grass map — optional

`uint8[(mapx / 4) × (mapy / 4)]` — one byte per **4×4 squares**, that is per 32×32 elmos. Present only when the vegetation extra header points at it.

See §5.

### 4.8 Tile index

At `tilesPtr`:

| Type | Field |
| --- | --- |
| `int32` | numTileFiles |
| `int32` | numTilesTotal — across all files |

Then, per tile file:

| Type | Field |
| --- | --- |
| `int32` | tile count in this file |
| `char[]` | file name, NUL-terminated |

Then the index itself: `int32[(mapx / 4) × (mapy / 4)]`, one entry per **4×4 squares** (32×32 texels — one tile), row-major. Each entry is a tile number, counted across all the listed files in order.

Several index entries may name the same tile. That is the format's compression: a map whose ground repeats stores each distinct 32×32 patch once.

### 4.9 Feature block

At `featurePtr`:

| Type | Field |
| --- | --- |
| `int32` | numFeatureTypes |
| `int32` | numFeatures |

Then `numFeatureTypes` NUL-terminated type names, then `numFeatures` records of 24 bytes:

| Type | Field | Notes |
| --- | --- | --- |
| `int32` | featureType | Index into the name list above |
| `float` | x | Elmos |
| `float` | y | **Ignored** — see below |
| `float` | z | Elmos |
| `float` | rotation | Heading, in degrees |
| `float` | relativeSize | **Ignored** |

Two fields do not reach the simulation: the engine places a feature at the **ground height under (x, z)** regardless of `y`, and it does not scale features, so `relativeSize` has no effect. Writing sensible values costs nothing and matches what other tools produce, but nothing reads them.

Names are read into a fixed `char[16384][32]` table, so there is a hard limit of **16,384 types** and **31 characters** per name.

Names beginning `TreeType0`…`TreeType15` and `GeoVent` are the engine's own built-in types. Maps conventionally list all seventeen at the head of the table whether or not they are used, because the engine's built-in handling refers to them by index — a map that omits them numbers its own types differently from every other map.

## 5. What the metal and grass bytes mean

These two are the most commonly misunderstood parts of the format.

### Metal

The metal map is **a field, not a list of spots.** There is no list of extractor sites anywhere in a map. An extractor collects whatever lies under its footprint — a disc of `extractorRadius` elmos, declared in `mapinfo.lua` — so a "spot" is simply a place where the field is high.

```
metal_at_cell = byte × maxMetal
```

`maxMetal` comes from `mapinfo.lua` and scales the entire map at once (`CReadMap::Initialize` → `CMetalMap::Init`). A byte is therefore relative: 255 with `maxMetal = 1.0` and 128 with `maxMetal = 2.0` yield the same metal. Its engine default is **0.02**, so a map that forgets to set it has essentially no metal on it however bright the map is painted.

Because yield is integrated over the footprint rather than sampled at a point, a **broad, smooth mound gives an extractor more than a single bright texel does**, and two mounds closer together than `extractorRadius` are partly shared. Maps normally paint a smooth dome per deposit for this reason.

### Grass

The grass byte is a **patch size, not a flag.**

This is worth stating plainly because the engine's own `CGrassDrawer` only ever tests the byte for non-zero — which is where the "grass is on or off" belief comes from. **BAR does not use that drawer.** It draws vegetation with `luaui/Widgets/map_grass_gl4.lua`, which reads the same byte through `Spring.GetGrass` and treats its magnitude as how large a clump grows.

Two values are special:

* **255 is reserved.** `Spring.GetGrass` returns 255 to mean *this map has no vegetation block at all*, so a real cell must never hold it.
* A map whose **highest byte is exactly 1** is treated as legacy binary grass, and every patch size is randomised.

So a map that wants graded vegetation should keep its values clear of both ends: somewhere in 2…254, with at least one cell above 1.

## 6. Terrain types

`mapinfo.lua` declares up to 256 of them, indexed by the type map's bytes:

```lua
terrainTypes = {
  [0] = {
    name = "Default",
    hardness = 1.0,
    receiveTracks = true,
    moveSpeeds = { tank = 1.0, kbot = 1.0, hover = 1.0, ship = 1.0 },
  },
  [1] = { name = "Sand",   hardness = 0.8, ... },
  [2] = { name = "Rock",   hardness = 1.4, receiveTracks = false, ... },
}
```

`hardness` multiplies the map's `maphardness` for cratering. `moveSpeeds` multiplies unit speed bymovement class. A type map of all zeroes — one uniform surface — is perfectly valid and is what most maps do.

## 7. Water

Water is rendered from `mapinfo.lua`. Its surface is a plane at **y = 0** and cannot be moved.

```lua
water = {
  damage      = 0,
  repeatX     = 0,  repeatY = 0,
  absorb      = {0.0, 0.0, 0.0},
  basecolor   = {0.0, 0.0, 0.0},
  mincolor    = {0.0, 0.0, 0.0},
  surfaceColor = {0.13, 0.28, 0.42},
  surfaceAlpha = 0.55,
  planeColor   = {0.07, 0.16, 0.25},
  diffuseColor = {...},  specularColor = {...},
  fresnelMin   = 0.2,
  fresnelMax   = 0.8,
  fresnelPower = 4.0,
  specularPower  = 20.0,
  specularFactor = 0.5,
  hasWaterPlane  = false,
}
```

Four of these are functions of the **viewing angle** rather than of depth, which is why they cannot be judged from a top-down picture. From `BumpWaterFS.glsl`:

```glsl
angle    = 1 - abs(dot(-eyeVec, normal))
fresnel  = FresnelMin + FresnelMax * pow(angle, FresnelPower)
specular = angle * pow(max(dot(reflectDir, eyeVec), 0), SpecularPower) * SpecularFactor
```

Note the first line: **`fresnelMax` is added to `fresnelMin`**, not the value reflection rises to. A surface seen edge-on reflects the sum of the two, so `fresnelMin = 0.4` with `fresnelMax = 0.8` saturates well before grazing incidence.

`voidWater = true` removes the sea bed entirely: ground below y = 0 is not drawn, and the map has a hole rather than a sea.

## 8. The `.smt` — Spring tile file

The ground texture, cut into tiles and compressed.

### Header — 32 bytes

| Offset | Type | Field | Notes |
| --- | --- | --- | --- |
| 0 | `char[16]` | magic | `"spring tilefile"` + NUL |
| 16 | `int32` | version | `1` |
| 20 | `int32` | numTiles | |
| 24 | `int32` | tileSize | **32** |
| 28 | `int32` | compressionType | **1** = DXT1 |

### Tiles

`numTiles` records follow, each **680 bytes**, in the order the `.smf`'s index refers to them.

One record is a 32×32 DXT1 image with mips down to 4×4:

| Level | Size | Bytes |
| --- | --- | --- |
| 0 | 32×32 | 512 |
| 1 | 16×16 | 128 |
| 2 | 8×8 | 32 |
| 3 | 4×4 | 8 |
| | | **680** |

DXT1 is 8 bytes per 4×4 block, and a 4×4 level is one block. There is no level below 4×4.

Because each tile is a fixed size, tile *n* is at `32 + n × 680` — the format needs no per-tile offset table.

### Sizing

The whole ground texture is `mapx × 8` by `mapy × 8` texels — 8 texels per square. An 8×8 map is therefore 4,096 texels square, and a 32×32 map is 16,384.

Before deduplication, tile count is `(mapx / 4) × (mapy / 4)`:

| Map | Texture | Tiles | `.smt` before dedup |
| --- | --- | --- | --- |
| 8×8 | 4,096² | 16,384 | ~11 MB |
| 16×16 | 8,192² | 65,536 | ~43 MB |
| 24×24 | 12,288² | 147,456 | ~96 MB |
| 32×32 | 16,384² | 262,144 | ~170 MB |

Identical tiles may share one record, and on maps with large uniform areas — deep sea, flat desert — this saves a great deal. Ground with fine noise everywhere deduplicates almost not at all, so this is a property of the texture rather than a guarantee of the format.

## 9. Reading order

A compiler must write the blocks in the order the header's pointers describe, because each pointer is an absolute offset that depends on the size of everything before it. The conventional order is:

```
header (80)
extra headers (0 or 12)
heightmap        (mapx+1) × (mapy+1) × 2
type map         (mapx/2) × (mapy/2)
minimap          699,048
metal map        (mapx/2) × (mapy/2)
grass map        (mapx/4) × (mapy/4)      — if present
tile index       8 + per-file entries + (mapx/4) × (mapy/4) × 4
features         8 + names + count × 24
```

A reader should use the pointers rather than this order, since nothing in the format requires it.

## 10. Summary of resolutions

One table, because the differing resolutions are the easiest thing to get wrong.

| Data | Grid | One cell covers |
| --- | --- | --- |
| Heightmap | `(mapx + 1) × (mapy + 1)` | a vertex, 8 elmos apart |
| Type map | `(mapx / 2) × (mapy / 2)` | 16 × 16 elmos |
| Metal map | `(mapx / 2) × (mapy / 2)` | 16 × 16 elmos |
| Grass map | `(mapx / 4) × (mapy / 4)` | 32 × 32 elmos |
| Tile index | `(mapx / 4) × (mapy / 4)` | 32 × 32 elmos |
| Ground texture | `(mapx × 8) × (mapy × 8)` | 1 × 1 elmo |
| Minimap | 1024 × 1024 | the whole map, stretched |

## References

The engine is the authority for everything above:

| Subject | Source |
| --- | --- |
| Map loading, block sizes | `rts/Map/ReadMap.cpp` |
| `.smf` and `.smt` reading | `rts/Map/SMF/SMFReadMap.cpp`, `SMFFormat.h` |
| Metal semantics | `rts/Map/MetalMap.cpp` |
| `mapinfo.lua` keys | `rts/Map/MapInfo.cpp` |
| Water shading | `rts/Rendering/Shaders/GLSL/BumpWaterFS.glsl` |
| Grass, as BAR draws it | `luaui/Widgets/map_grass_gl4.lua`, in the BAR game repository |

Recoil: <https://github.com/beyond-all-reason/RecoilEngine> ·
BAR: <https://github.com/beyond-all-reason/Beyond-All-Reason>
