The Map Builder on this site generates playable Total Annihilation maps entirely in your browser. This article documents how it works β the same format knowledge from Reverse Engineering TA Files, applied in the opposite direction: instead of parsing a map, we write one.
The trick: borrow the tile atlas
A TNT file contains three big blocks: the tile map (which tile goes where), per-cell attributes (height, features), and the tile atlas β the actual 32Γ32-pixel tile graphics, unique per map. A big map carries thousands of unique tiles, easily 10+ MB.
Creating tile graphics from scratch is a content problem, not a format problem β that’s where AI-generated maps (and several failed generator attempts) produce the infamous “random tile noise”. Our builder sidesteps it entirely: it borrows the complete tile atlas of Ancient Issaquah, a Cavedog map whose terrain covers green land, cliffs, shores and water. Every tile in the atlas is indexed with its elevation profile (the four height values of its 2Γ2 attribute cells), learned from the original map.
That gives the generator something no text model has: a vocabulary of tiles that are known to look good, each tagged with the terrain height it represents.
The pipeline
- Heightmap β value noise over the map grid (one height per 16-px attribute cell), with an optional island falloff that pushes the map border below sea level.
- Tile matching β for every 32Γ32 block (2Γ2 cells), look up the atlas tile whose elevation profile best matches the four generated heights (minimum absolute difference). The result is consistent terrain: shores get shore tiles, heights get highland tiles β because that’s what the atlas tiles represented in the original map.
- Attribute block β the chosen tiles’ height values are written into the attribute cells verbatim, so the visible terrain and the walkable heights agree exactly. Features are set to none (0xFFFF).
- Assembly β 64-byte header (pointers to each block), tile map, attributes, only the used subset of the atlas, then the minimap (252Γ252 palette indices, water/lowland/highland colored).
- Companion OTA β a matching mission file with four ring-layout start positions, generated by the same rules as the OTA Builder.
Header field 0x2c: the flag that nobody documents
Our first generated map parsed perfectly, rendered a minimap β and crashed
Total Annihilation on launch. The culprit: header field 0x2c (kbot calls
it Unknown1), which we had set to 0. Checking every real TA map we have:
the field is 1 in all of them, without exception (195/195). The engine
evidently requires it to be 1 for a well-formed TA map; write 0 and the
game dies before the minimap is even considered.
Two more conventions we confirmed the same way, by comparing 195 maps:
- Maps without feature entries (
TileAnims = 0) point the feature pointer at the minimap block (or one byte before it) rather than leaving it null. - The minimap’s void padding byte for non-square maps is
0x64(100); palette values below it are “water-ish” territory in practice.
This is exactly why the builder is verified against the archive: the format knowledge exists in no single document, only in 600 files that happen to agree.
Step tiles and walkability (offline generator, 2026-09-05)
The browser builder matches tiles to arbitrary heights. For free-form terraced terrain we went the opposite way: quantize a value-noise heightmap onto the green tileset’s quasi-flat levels (191/208/224/236/255 β no tileset has perfectly flat tiles at each level; “flat” means Β±8 corner spread), then fill every transition with real step tiles from a step inventory extracted from the atlas.
Hard-won details:
- Step tiles have real corner heights, not nominal ones. A “191β208”
step tile’s corners are e.g.
[197, 191, 206, 202]. Writing nominal level values (191/208) into the attribute cells instead of the tile’s own profile creates phantom cliffs at every flat/step boundary β measured max step jumped from 30 to 46 height units. Always copy the tile’s profile verbatim into the attributes (the browser builder already did this; the offline generator learned it the hard way). - Walkability budget: keep per-cell corner steps β€ ~25, ideally β€ 17. After tight profile-based tile selection the whole map sits at p99 = 17, max = 30 (523k cell pairs, 16 of them over 25). Vanilla movement classes span MaxSlope 8β36 (see Map Editor Research); steps of β€ 25 per 16-elmo cell stay inside practically every tank class.
- Selection scoring matters: with only 2β6 green step tiles per
transition group, picking by
|min(profile)βlo| + |max(profile)βhi|(with slight random variety) eliminates the boundary-cliff problem entirely β no synthesized heights needed.
Two free test maps from this pipeline (noise-seeded terraces, 4 start
positions): Terra_16x16_s7 and Terra_16x16_s11 in the
download archive.
On-the-fly synthetic tilesets (2026-09-05)
The atlas-borrowing approach has a hard ceiling: tile shading is baked into
the borrowed graphics, and the green step tiles of Ancient Issaquah carry
only weak shading (luminance spread 15β45/255 across a tile) β slopes are
feelable (the engine slows units per height rules) but invisible.
Since a TNT embeds its own tile graphics, nothing stops us from
inventing a tileset at generation time (_gen_synth.py):
- Palette ramp: TA’s PALETTE.PAL contains a contiguous green ramp (indices 44β162, luminance 33β150). A tile renderer maps height + slope shading onto that ramp β brightness literally encodes elevation.
- Baked slope shading: light from NW; per-pixel brightness = interpolated corner height + NW-ward gradient term. Slopes now read visually while staying walkable β the “bewalkable = looks flat” trade-off of borrowed tiles disappears.
- Grass texture: two-octave noise (1px fine grain + smoothed 4px clumps, Β±3 ramp steps) avoids the flat plastic look.
- Only-used-tiles embedding: the tile block contains exactly the generated variants (116 for a 16Γ16 map) β synthetic maps come out ~30% smaller than atlas-borrowed ones.
Minimap pitfall: the minimap is 252Γ252 palette indices, not luminance. Writing raw height-derived brightness values produces wild colors β it must map height through the same palette ramp as the tiles.
Open: texture variety (dirt/rock sprinkles via a second ramp), synthetic cliff tiles, and porting the shading idea back into atlas-based maps.
AI-generated image maps (2026-09-05)
The pipeline went from borrowing tile atlases to painting the entire map as an image and slicing it into tiles β the Cavedog workflow, automated:
- Color image (any AI generator, orthographic top-down prompt) is rescaled to the map pixel grid and mapped onto PALETTE.PAL (nearest color, 5-bit spatial hash).
- Grayscale heightmap (generated as a companion image in the same AI session so composition matches) rescaled to the attribute cell grid, blurred, minima compressed (craters must not become impassable holes), quantized to 8-unit steps with a walkability relaxation pass (max 24 per cell).
- Feature map (optional third image, signal colors on black: green = tree, red = vent, white = metal pad, gray = rock) is sampled per attribute cell and written into the attr feature indices.
- Paint-then-slice: the whole image is rendered first, then cut into 32Γ32 tiles β tile-boundary artifacts are impossible by construction.
- Start positions are auto-placed on the 4 flattest, far-apart land cells.
Gotchas learned in-game:
- Sea level semantics: TA renders everything below
sealevelas water. Setting it to 255 flooded the whole map (slow Commander, blue contours). It must sit below the minimum land height β or at a deliberate water threshold for lakes/rivers. - Contours vs. graphics alignment: AI image pairs have a consistent offset; a fractional heightmap shift (bilinear roll, +8 cells for Gemini) calibrates contour lines onto the painted terrain.
- Metal world trap:
SurfaceMetal=128; MohoMetal=255;in the OTA makes the entire map a metal world. Normal maps use ~6/40 (Sherwood values). - Dedup reality: photo-detailed images produce almost no duplicate tiles (64k unique on 16Γ16 β 67 MB file). Practical fixes: smaller maps, softer palette mapping, or texture-quantizing the source image.
- Feature table format (from Sherwood): N Γ 132-byte records after the
minimap β 4 pad bytes + 32-byte name + 96 zeros;
header[7]= count,header[8]= pointer. Positions live ONLY in the attr bytes 1β2 as the table index (no coordinates in the table).
The whole pipeline runs client-side at β upload three images, get TNT+OTA.
- Metal spots and features (trees, rocks, wreck props) are not placed β open the generated map in Mappy or Annihilator to add them.
- Texture variety comes solely from the Issaquah tileset; a desert or snow generator would need a different atlas.
- Vergence/slope smoothness is as good as the elevation matching gets β occasional tile seams are possible where the height jump between neighbours is large.
Try it
Head to the Map Builder, pick a size and seed, hit
Generate, download .tnt + .ota, drop both into your TA folder and
start a skirmish. If a generated map looks off in-game, that’s a bug we
want to hear about β contact in the Imprint.