Maps (cook level geometry → server)
A Citadel map is a small binary file (.map, CMAP format) that carries one
level’s static collision geometry in world space. The server loads every map in
its maps_dir at startup and indexes it by file name; when a
room is created, the room’s server-chosen map name resolves to a loaded
map. That geometry is the input the server uses for authoritative collision and — in
a later phase — its own baked navmesh, so server-driven
networked actors (NPCs) can path around the real level.
Pipeline
Section titled “Pipeline”Unreal editor: Tools → Citadel → Cook Map Data → writes MyLevel.map (world-space collision triangles) → drop into <server>/maps/ (config: runtime.maps_dir) → server loads it at startup, indexed as "MyLevel" → room's on_room_create → { map = "MyLevel" } resolves to itThe map name the server matches is the .map file’s stem (MyLevel.map →
MyLevel), so it must equal the name your on_room_create hook returns.
Tiled TMX (2D)
Section titled “Tiled TMX (2D)”For a top-down 2D game the server can load a finite, orthogonal .tmx file from
maps_dir directly, or convert it before deployment:
citadel cook-tmx --input GlobalGrove.tmx --output maps/GlobalGrove.mapMark a tileset tile with the Boolean property citadel_collision=true, or put
rectangle/convex-polygon objects in an object layer named collision (a layer
property of the same name is also accepted). TMX X/Y pixels become Citadel X/Z;
walls are extruded upward, with one pixel equal to one centimetre by default.
Set citadel_units_per_pixel on the map to change that scale. Isometric,
infinite, rotated/flipped, and unsupported collision shapes are rejected rather
than silently losing authoritative collision.
Tiled workflow: collision in five minutes
Section titled “Tiled workflow: collision in five minutes”Use this recipe for a map that Citadel can validate and load without any custom exporter.
- In File → New → New Map, choose Orthogonal and leave Infinite
disabled. Set the tile dimensions to the same size as the tileset you plan to
render (for example
32 × 32). Save the map asGlobalGrove.tmx. - Add the tileset as an external
.tsxwhere possible. This keeps tile metadata (including collision properties) shared between every map that uses it. - Pick one collision authoring method below. The object-layer method is usually the quickest for walls, cliffs and irregular blockers; the tile-property method is best for grid-aligned solid tiles.
- Select the map in Tiled’s Properties panel and add a
floatproperty namedcitadel_units_per_pixelwhen your world scale is not one centimetre per pixel. For example, set it to0.01when one Tiled pixel represents one centimetre in a metre-based game world. - Run the cook command below during CI or before copying the map to the server. The command fails loudly for unsupported content, so a broken collision map is caught before it reaches players.

Screenshot from the official Tiled Custom Properties documentation, used here as a visual reference for the editor UI.
Option A: collision object layer (recommended)
Section titled “Option A: collision object layer (recommended)”Create an Object Layer named exactly collision. Draw visible rectangles or
convex polygons for walls and blockers. Citadel turns each shape into a vertical
collision prism; it does not render the layer or send its geometry to clients.
You may use another layer name if you give that layer the Boolean property
citadel_collision=true. Keep collision shapes axis-aligned: rotated objects,
ellipses, points, polylines and concave polygons are rejected.

Screenshot from the official Tiled Layers documentation. Lock the collision layer once authored to avoid accidental edits.
Option B: solid tiles
Section titled “Option B: solid tiles”Select each solid tile in the tileset and add the Boolean tile property
citadel_collision=true. Every placement of that tile produces a collision prism.
Do not flip collision tiles in a tile layer: horizontal, vertical and diagonal
flips are deliberately rejected, because they can silently change authoritative
geometry.
Tile collision objects embedded in a tileset are also supported when they are rectangles or convex polygons. This is useful when the blocking area occupies only part of a decorative tile.
Supported subset
Section titled “Supported subset”| TMX feature | Citadel behaviour |
|---|---|
| Finite, orthogonal map | Supported |
| External TSX tileset | Supported; resolved relative to the TMX file |
collision object layer or layer property |
Supported |
| Rectangles and convex polygons | Supported |
citadel_collision=true tile property |
Supported |
| Isometric or infinite map | Rejected |
| Rotated/flipped collision content | Rejected |
| Ellipse, point, polyline or concave polygon collision | Rejected |
Validate and deploy
Section titled “Validate and deploy”Start with the minimal collision-only example. It contains one rectangle and one convex polygon, so it can be cooked without a tileset:
citadel cook-tmx \ --input citadel-collision-example.tmx \ --output maps/GlobalGrove.mapFor production, prefer the cooked .map: it makes startup input deterministic and
avoids parsing editor assets on the server. Direct .tmx loading is useful during
iteration; copy either the .tmx (and its relative .tsx/image assets) or the
cooked .map into runtime.maps_dir, then restart the server. The map filename
stem must match the map returned by on_room_create.
Cook a .map from your level
Section titled “Cook a .map from your level”The cook tools ship as editor-only tools (they are never compiled into a packaged client). Each engine re-bakes its own static scene geometry into the engine-agnostic CMAP format.
The tool is a menu action, not code. With the Citadel plugin enabled:
- Open the level you want to cook.
- Tools → Citadel → Cook Map Data.
- In the Save As… dialog, pick a location and name. The default file name is
the level’s name (e.g.
Lvl_ThirdPerson.map) — keep it matching themapyouron_room_createreturns. - A notification reports the exported vertex/triangle counts and the output path.
The tool scans every static, collidable StaticMeshComponent in the level
(including instanced meshes) and Unreal Landscape collision-heightfield tiles,
transforms them to world space, and writes one combined triangle mesh. Landscape
visibility holes are omitted and its alternate collision-cell diagonal is retained.
The exporter reads collision rather than render topology, so Merge Actors remains an
optional artist workflow rather than a requirement. Components with collision
disabled are skipped; a level with no static collision reports “nothing to export”.
Same tool — it is a top-level editor menu action, so there is no Blueprint graph:
- Open the level.
- Tools → Citadel → Cook Map Data.
- Save As… with a name matching your room’s
map(default = level name). - Read the exported vert/tri counts in the confirmation toast.
Copy clients/unity/Editor/CitadelCmapExporter.cs under your project’s
Assets/Citadel/Editor/, then select Tools → Citadel → Export CMAP Map….
It exports static MeshCollider sources and static Terrain heightfields to
one CMAP collision mesh. Terrain holes are omitted; source vertices are welded
at a fixed 0.001 world-unit tolerance and degenerate triangles are dropped.
Sources are processed in stable scene order. The tool rejects singular source
transforms, non-finite world vertices, and maps over its 10-million-triangle
safety cap rather than emitting a partial file; it reports normalization counts
in the Unity Console.
The Godot SDK contains an editor plugin at
addons/citadel_map_exporter/. Copy it under your project’s res://addons/,
enable Citadel Map Exporter in Project → Project Settings → Plugins, then:
- Open the saved 3D level scene.
- Select Tools → Citadel → Export CMAP Map….
- Choose a
.mapoutput path and keep its filename matching themapyouron_room_createreturns. - Copy the result into the server’s
maps_dirand restart the server.
The exporter combines MeshInstance3D nodes under StaticBody3D into a
world-space triangle mesh. To include a mesh outside a static body, set its
citadel_export_collision metadata to true; set it to false to exclude an
otherwise included mesh. A terrain addon can opt in by implementing
citadel_cmap_terrain and returning the documented collision-height grid
(width, depth, heights, size, optional byte-per-cell holes). The
exporter welds at 0.001 units, rejects non-finite data and maps over 10
million triangles, and prints normalization counts in the Godot Output panel.
Dynamic bodies, CSG, render-only terrain displacement, and procedural meshes
are not collected automatically.
Install the map on the server
Section titled “Install the map on the server”Copy the cooked file into the server’s maps directory — by default ./maps
relative to the working directory (runtime.maps_dir). The
directory is created on first run if absent.
<server working dir>/ citadel.toml game/main.lua maps/ Lvl_ThirdPerson.map ← the cooked fileAt startup the server scans this directory, decodes each .map, and logs what it
loaded:
map catalog loaded loaded=1loaded map map=Lvl_ThirdPerson verts=… tris=…Malformed or unreadable files are skipped with a warning — a bad map never stops the
server. When a room resolves a map name with no matching file, the server warns and
lists the maps it does have (catches typos and uncooked levels).
Trusted server logic can inspect a loaded map without receiving its raw geometry:
citadel.map_info(name) returns its bounds and collision vertex/triangle counts,
or nil (Lua), None (Python), or null (JavaScript) when it is not loaded.
File format (CMAP)
Section titled “File format (CMAP)”.map is a small, versioned, section-framed big-endian binary format
(implemented by the citadel-map crate). A reader skips sections it does not
understand, so future data (e.g. baked navigation) can be added without breaking
older servers.
| Part | Contents |
|---|---|
| Header | magic CMAP + format_version (u32) |
METADATA section |
level name (u16-len UTF-8) + world-space AABB (bounds_min/bounds_max, f32×3 each) |
COLLISION section |
vertex count (u32) + vertices (f32×3) + triangle count (u32) + triangles (u32×3 indices) |
NAVMESH section |
optional server-baked Detour tile, guarded by Detour version and polygon-reference width |
Coordinates remain in the source engine’s world units. Configure the server-side actors and transform-sync values in the same convention as the exported level so server-driven NPC positions line up with the cooked geometry with no conversion.
Known limitations
Section titled “Known limitations”- Terrain-aware exporter workflow. Unity exports built-in Terrain, Unreal
exports Landscape collision heightfields, and Godot supports explicit terrain
providers. Terrain uses collision-height samples and holes, not trees,
grass/details, material displacement, or runtime deformation. The
.mapformat itself is engine-agnostic. - Static collision only. Unreal exports static meshes and Landscape collision
heightfields, but not BSP/geometry brushes, skeletal meshes, or procedural
collision. Godot exports static-body
MeshInstance3Dnodes (or explicitly tagged meshes) plus an explicit terrain provider, not CSG or dynamic-body collision. - Navigation is static and server-side. Detour bakes/query uses the cooked collision mesh. Dynamic obstacles, off-mesh links, and server-side player-collision validation remain future work.