The .tdm map format¶
These pages are for you if you're writing a tool that reads or writes .tdm files, like an importer, a converter or a
CI check. You don't need any of this to build levels.
A .tdm file holds three things: the worldspawn properties, a bit of editor state, and a tree of nodes with the actual
content.
The editor saves it as a binary container of zstd compressed chunks, so files stay small and a damaged file still
opens. Older editors saved one UTF-8 JSON document instead, and every reader loads both. Both forms hold the same tree,
so these pages show it as the JSON that trenchdot --dump prints.
| Page | Read it when you |
|---|---|
| Container layout | Parse or write the binary file. It covers the file header, the chunks and how each value is encoded as bytes |
| Damaged files | Want to know what still loads from a corrupt file, and where each reader reports what was lost |
| Nodes | Walk the tree. It explains the shape every node shares, how ids work, and the layer, group, entity and instance nodes |
| Geometry | Read or build brushes and meshes, including their faces, texture projection and displacements |
| Terrain and scatter | Handle heightmap terrains and sets of scattered models |
| JSON layout | Work with the readable form, convert between the two forms, or read what the editor puts on the clipboard |
Want to see working code? The reference implementation is
format.rs, with the container in
binary.rs and
variant.rs. On the Godot side,
tdm_file.gd reads the file and
tdm_parser.gd builds it.
Top level¶
| Key | Type | Written | Meaning |
|---|---|---|---|
format |
string | always | Always "trenchdot-map". It tells a map apart from other JSON |
version |
integer | always | Map version, currently 1 |
properties |
object of string to string | when not empty | Worldspawn keys, sorted by name |
editor |
object | when not all default | Editor view state. It is never exported to Godot |
textures |
object | when any texture has settings | Texture settings by material name |
layers |
array of nodes | always | The map's layer nodes, in order. Everything else hangs below them |
lightmap |
object | JSON maps with a bake | The baked lighting, which a binary map keeps in its LMAP chunk |
Only layer nodes are read from layers, and a map without any gets a Default layer when it loads.
Worldspawn values are always strings, like in a .map file. When Godot builds the map, TrenchDotEnvironment turns
these keys into a sky, fog and a sun:
| Keys | What they control |
|---|---|
sun_angles, sun_color, sun_energy |
The directional sun light: where it points, its color and how bright it is |
ambient_color, ambient_energy |
Light that reaches surfaces the sun does not, so shadows are not pitch black |
sky_top_color, sky_horizon_color, sky_ground_color, sky_energy |
The procedural sky gradient and its brightness |
sky_panorama |
A res:// image that replaces the procedural sky in Godot. The editor preview and its bake keep using the sky colors |
fog_color, fog_density |
Distance fog |
glow_intensity, ssr |
Bloom on bright surfaces, and screen space reflections when ssr is 1 |
If your game brings its own WorldEnvironment, set environment to sun_only and only the sun gets built. 0 or
none builds nothing, see environment and sun. sky_source remembers
which texture the sky faces of an imported map had.
The editor object is view state that only matters inside the editor:
| Key | Type | Default | Meaning |
|---|---|---|---|
cameras |
object, keys "1" to "9" |
empty | Saved camera positions, each {"position": [x, y, z], "yaw": r, "pitch": r} with angles in radians |
cordon |
{"min": [x, y, z], "max": [x, y, z]} |
none | The cordon box in map units, used to work on one part of a big map |
cordon_enabled |
bool | false |
Hide objects outside the cordon and leave them out of cordoned exports |
bake |
{"quality", "texel_size", "softness", "backend"} |
none | The options the map was last baked with, so the next bake starts from them |
graph_frames |
array | empty | Frames of the Logic graph, each {"title", "rect": [x, y, width, height], "color"} in graph points. A damaged one is dropped on load |
Versions and compatibility¶
format and version describe the data. The container version in the file header is separate, it only changes when
the chunk layout does.
Here is what each reader does with a file it doesn't fully understand:
| Situation | Editor | Godot addon |
|---|---|---|
Other format |
Refuses the file | Build error |
Newer version |
Refuses the file | Build error asking for an addon update |
Missing version |
Reads it as the oldest version | Same |
| Unknown key | Ignores it, and it is gone after the next save | Ignores it |
| Unknown chunk | Keeps it and writes it back on save | Skips it |
| Face index out of range | Refuses the map, naming the node | Skips that brush |
Displacement heights of the wrong size |
Refuses the map, naming the node | Builds the face flat |
The editor drops unknown keys when it saves. So if you add data that an older editor must not lose, you need either a map version bump, which older editors refuse to open, or a chunk of its own, which they keep untouched. See Other chunks.
Coordinates and units¶
Positions use Godot's axes: Y up, X right, Z towards the viewer. The build divides by the map settings'
inverse_scale_factor, 32 by default, so 32 map units make one meter.
| Quantity | Unit |
|---|---|
| Positions, sizes, heights, spacing, cell size | Map units |
| Entity, instance and scatter angles | Degrees: pitch around X, yaw around Y, roll around Z, applied in YXZ order like Godot's rotation_degrees |
| Camera bookmark yaw and pitch | Radians |
| UV offset | Texels |
| UV scale | Map units per texel |
Mesh uvs |
Texture widths, so 1 is one full repeat of the texture |