Maps are Blender files
There is no level editor to install and no SDK to learn. Every map in the game ships with the .blend it was built from, so the shortest way in is to copy one of those folders and start moving walls. If you can model a room in Blender and name a handful of objects, you can play a match in it a few minutes later. This page is the whole workflow, start to finish.
Copy a map that already works.
Every map in the game ships with the Blender file it was made from. Copy one of those folders, change what you want, run one command. Everything further down this page is detail you can pick up when you need it.
cp -r data/maps/dizzy_raccoon data/maps/dusty_heron
rm data/maps/dusty_heron/build.py data/maps/dusty_heron/map_baked*
rm -rf data/maps/dusty_heron/hdris
./client --host-map data/maps/dusty_heron/map.blend --skip-bakeThat is a playable map of your own, in the game, before you have opened Blender once. Everything you deleted in the second and third lines regenerates from your .blend: they are the previous map's leftovers, and they are worth clearing so none of it outlives the map it belonged to.
1. Copy a map folder
Every map in the game ships with its map.blend, and
they are all sitting in data/maps/. Copy whichever
one you like the shape of, name the copy after your map, and you already have spawns for both
teams, both bomb zones, the sun, a sky dome, a reflection probe, an ambience bed and tagged
surfaces in place.
2. Change it
Open the copy in Blender 4 or 5 and rework it as far as you want, up to knocking the whole thing down and building your own in its place. What you model is what players collide with - there is no separate collision mesh and no compile step. Blender is Z-up and the game is Y-up, and the exporter handles that for you.
3. Keep the named objects
Move the markers rather than deleting them. Spawns for both teams, a bomb zone each and one
hdri_default probe are what a match needs.
Everything else - sounds, surfaces, foliage, shaders - is opt-in, and all of it is done by
naming the object.
4. Run one command
Point the client at the .blend. It exports, bakes lighting with Cycles, captures reflection probes, starts a server in-process and drops you into your own map. Add bots and you are playing a real match in it minutes after you started.
Which one to copy
- Any map in the pool
- A real arena people play, and the one to copy if you want a layout to push around. Their .blend files are machine-written from a seed, and they carry spawns, both bomb zones, the sun, a sky dome, a probe, an ambience bed and surface tags. They carry no kill_zone, no area callouts and no clip volumes, so expect the checker to ask you for those.
- _example_map
- Greybox rather than a map you would play, and the only file with the whole vocabulary in one place: a kill_zone, six area callouts, per-zone ambience on top of the bed, all thirteen surface tags on demo cubes, wind-driven grass, clip volumes, bomb-site markers, a zone reflection probe and two custom shaders. Open it to see what a convention actually looks like in Blender. The underscore keeps it off every ballot: the map vote skips
_-prefixed folders, so copy it to a folder of your own to turn it into a map people can play.
build.py is the generator that wrote that .blend. Leave it in place and the client will re-run it the moment it is newer than your map.blend, overwriting your work with the original seed. Delete it from your copy.
hdris/ holds the probe faces captured inside the map you copied. They are only re-captured when one is missing, so a copied set quietly reflects the old map forever. map_baked.glb is the same story for geometry and lighting: it is what other players and the in-game map vote load, and it stays the old map until you run a real bake.
Blender file in, playable match out.
One command exports, bakes, captures probes, starts a server and connects you. There is no build step to run first and no editor to learn.
./client --host-map data/maps/dusty_heron/map.blendThat single command does, in order:
- 1. Export
- The .blend is exported to glTF through the shipped export script - sun direction, spawns, zones, surface tags and tangents all come across.
- 2. Bake
- A full Cycles lightmap bake at 128 samples across every CPU core and your GPU. Minutes on the first run, instant on every run after that.
- 3. Capture probes
- If any HDRI probe face is missing, the client captures them from the live scene before letting you play - a few seconds with the HUD hidden.
- 4. Host
- A server starts inside the client process on port 27015 and the client connects to it. Same server code a dedicated box runs, so everything is exercised exactly as it will be in a real match.
Bake modes
- --skip-bake
- No Cycles bake. Launches in seconds on sun and ambient light only. This is the mode to live in while you are moving walls, placing spawns and testing sightlines.
- --fast-bake
- A one-sample Cycles bake: noisy, but it shows you the shape of the baked lighting in seconds rather than minutes.
- no flag
- The ship-quality 128-sample bake. Run it once the geometry has settled, or before you share the map with anyone.
- --no-bake-gpu
- Bake on CPU only - useful when the GPU is busy with something else.
- --bake-workers N
- Override the automatic worker count.
- --force-export
- Ignore the bake cache for one run.
The bake cache
Every successful export is recorded in ~/.cache/proaim/map_bakes.json against the .blend's path, modification time and bake mode. Re-running on an unchanged file at the same mode short-circuits the whole pipeline and connects instantly. Saving the .blend invalidates that one map, and updating the game re-bakes every map.
Already have a .glb? --host-map data/maps/<name>/map_baked.glb skips Blender entirely.
Ask what is wrong before you bake.
One command reads your .blend and reports every problem the game would have with it, in seconds, without baking anything.
./client --check-map data/maps/dusty_heron/map.blendThis reads your .blend, checks everything the game is going to require of it, and prints every problem it found in one go. It does not bake, does not export and does not start a server, so it takes seconds. Run it whenever you are not sure, and run it before you send the map to anybody.
Each problem comes with the object it is about and what to do in Blender. A clean map says All map checks passed.
What it checks
- Everything is named right
- Spawns, both bomb zones, the sky dome, a Sun lamp, one hdri_default probe, one map-wide ambience, and every ambience / area / probe / shader name parsed the way the game will parse it.
- Spawns are standable
- Each spawn marker is tested the way the game will: is there floor under it, is that floor real collision rather than decoration, is the marker sunk into it, is there room to stand without being inside a wall. This is the check that catches "everyone spawns falling".
- Bomb sites are plantable
- Each zone is sampled to confirm somewhere inside it a player can actually stand, so a site cannot end up floating over the scenery.
- Materials will survive export
- Every visible mesh has a material, no object has a negative scale, and nothing has UVs too broken to render.
- Textures are packed
- An unpacked texture is a path on your machine. It looks right for you and arrives missing for everyone else.
And what it tells you
After the checks it prints the size of your map: mesh count, triangles, materials, texture megapixels, the bounding size of the level in metres and a tally of every marker it found. The marker tally is the quickest way to notice you have five red spawns and four blue ones.
The same checks run automatically as part of a normal --host-map export, and stop it before the bake if anything fails. --check-map is just the fast way to ask without waiting for Blender to do the rest.
How big the player is.
Blender units are metres and the player is built to a fixed set of them. Doorways, cover and ledges are all judged against these numbers.
Blender units are metres, and the player is built to these. Doorways, cover, ledges and gaps are all judged against them, so these are the numbers to model with.
- 1.8 m tall, 0.8 m wide
- Standing. The player is a capsule 0.4 m in radius, so a gap under 0.8 m across is a gap nobody fits through.
- 1.2 m crouched
- Crouching lowers the whole body, so 1.2 m of headroom is the minimum for a space a player can be in at all.
- Eyes at 1.7 m
- Where the camera actually is. Cover you want to shoot over goes below this, cover you want to hide behind goes above it.
- Steps up to 0.5 m
- Anything this tall or shorter is climbed by walking into it, with no jump. Stair risers, kerbs and low ledges want to be under this. Anything taller reads as a wall.
- Jumps 1.05 m
- A standing or running jump lifts the feet 1.055 m, so that is the tallest thing reachable with a plain jump.
- Crouch-jumps 1.66 m
- Ducking in the air pulls the feet up another 0.6 m. This is the highest a player can get onto unaided, and it is the number that decides whether a roof is reachable.
Speeds, if you are timing something
- 6.0 m/s running
- The default. A 60 m corridor is ten seconds of running.
- 2.65 m/s walking
- Holding shift. Silent, and well under half speed.
- 1.85 m/s crouched
- Also silent, and slower still.
Bots are slightly more cautious than players about stairs: they path up steps to 0.48 m rather than the full 0.5 m. If bots refuse to walk up something a human climbs fine, that 2 cm is why. Build stairs with a bit of margin and both are happy.
Spawn markers are the one place where height has to be exact rather than close. The marker's origin is the player's feet, so put it on the floor surface. --check-map measures every one of them and tells you the Z to use.
One folder per map.
The folder name is the map's identity. Put the .blend inside it and everything else is generated beside it.
proaim/
client, server, README.txt the shipped binaries
data/
maps/
dizzy_raccoon/ one of the maps that ship
map.blend the source, copy this folder
build.py the generator, delete it in your copy
map_baked.glb regenerated
map_baked.png regenerated lightmap atlas
hdris/ regenerated probe faces
dusty_heron/ your map
map.blend your source of truth
shaders/ optional, custom shaders for this mapThe folder name is the map's identity, and the files inside it are generic. The exporter refuses to run on a .blend that lives anywhere else, because that is where the map's name comes from.
That also makes the folder the unit you hand to somebody else. A shader you write for this map lives in its shaders/ subfolder and is found before the shipped ones, so the map arrives complete rather than as a map plus a list of files to go and install.
The .blend and anything in shaders/ are yours to keep - everything else regenerates. Textures must be packed into the .blend (File → External Data → Pack Resources): the exporter checks, and stops with the unpacked files named rather than shipping a map with textures missing.
Naming is the scripting language.
There is no scripting layer and no entity editor: an object's name is what tells the game what it is. This is the whole vocabulary.
- spawn_red_0, spawn_blue_0, ...
- Team spawn points. The marker's origin is the player's feet.
- bomb_zone_red / bomb_zone_blue
- The bomb site a team defends. The attacker plants on the defender's zone, exactly like Ts planting on a CT site - so bomb_zone_red sits near red's spawn.
- bomb_marker_red / bomb_marker_blue
- Author-time visual markers for those zones. Hidden at runtime.
- kill_zone
- An instant-death volume. The bottom of the map, or anywhere else a fall should end.
- no_col*
- Drawn, but nothing collides with it - decorative grass, rubble, clutter.
- invisible_col*
- Collides, but is never drawn - clip boxes, knee walls, ceiling caps.
- sky_dome*
- The skybox. Never collides, never bakes, and its authored material is what reflects in chrome.
- sun_main
- The Sun lamp whose direction, colour and energy are baked into the exported map. Your map needs exactly one Sun lamp. It is picked because it is a Sun, not because of its name, so
sun_mainis a convention rather than a requirement. - hdri_default
- Required, exactly one. The map-wide reflection probe.
- hdri_<easing>
- Optional per-zone reflection probe. The cube's box is where it takes over from the default.
- ambience_<db>_<easing>_<stem>
- A looping ambient sound zone.
- area_<Callout>
- A named place. Standing inside this box puts its name on the HUD and on your team's radio, so
area_Mid_Towerreads as "Mid Tower". Underscores become spaces and capitals are kept. - material_<tag>_*
- Surface tag driving both bullet-impact and footstep sound banks.
- movement_grass_* / movement_foliage_* / movement_tree_*
- Opt this mesh into wind sway at one of three scales.
- shader_<kind>_<stem>
- Swap this mesh onto a custom shader: <stem>.ron from your map's own shaders folder, or the shipped data/shaders. <kind> is
bake,dynamicorno_shadowand is not optional.
Prefixes compose: no_col_shader_no_shadow_ocean1 is both collision-free and running the ocean shader with no shadow. Blender's .001 / .002 duplicate suffixes are handled for you.
Marker meshes should not light the map. Per marker, in Object Properties → Visibility → Ray Visibility, uncheck everything except Camera.
The four objects a match needs.
Spawns for both teams and a bomb zone each. Get the heights and the ownership right and the map is playable. Get them wrong and every round starts badly.
- Height matters
- A spawn marker's origin is where the player's feet appear. Set its Z to exactly the floor surface: too low and players start inside the floor, too high and every round begins with a drop.
- Which zone is whose
- bomb_zone_red is the site red defends and blue attacks. Put it near red's spawn and bomb_zone_blue near blue's, then check it in game - halftime swaps the sides, so both have to work from both ends.
- Plantable surface
- The plant happens where an attacker can stand inside the zone, so a site needs floor a player can actually stand on rather than a volume floating over scenery.
- Count them
- Give each team as many spawn markers as the biggest match you expect - five a side is the default team size.
Light it in Blender, play it in the game.
Lighting is a full Cycles bake, so the room you lit is the room you play in. The only rule is to light it the way the pipeline expects.
- One sun
- Name your Sun lamp sun_main. Its direction, colour and energy are read at export and become the game's directional light, so the shadows you set up in Blender are the shadows you play in.
- The lightmap
- Everything static is baked to a lightmap atlas with Cycles and shipped alongside the map. That is where bounce light, soft shadow and colour bleed come from.
- What is not baked
- Foliage that sways and any marker mesh is excluded automatically - moving geometry cannot be represented by a static bake.
- Sky texture
- Do not light the map from a World Sky Texture: it would add a second phantom sun to the bake, and the exporter strips it. Light with sun_main and let the sky_dome mesh carry the look.
Iterate with --skip-bake, then bake properly when you want to actually look at the place. A full bake is minutes; it is not something to sit through between wall moves.
What the shiny things reflect.
Reflections come from cubemaps captured inside your own level at runtime. One probe is mandatory, and the rest are for rooms that light differently.
Anything shiny - chrome, glass, polished metal - reflects a cubemap captured from a probe you place in Blender as a small named cube. You never render these in Blender: the game captures them from the live scene, including your sky dome, the first time the map runs.
- hdri_default (required)
- Exactly one per map. A one-metre cube at roughly eye height in the most average open part of the level. Its capture is what reflects everywhere the player is not inside a zone probe.
- hdri_<easing> (optional)
- Any number of zone probes. The cube's box is where that probe takes priority - a dim warehouse against the sunlit street, a neon basement against a daylight roof.
- Easing
- soft is a smoothstep and the usual choice, medium is linear, hard is quadratic for sharp room boundaries, and instant is binary in space and only smoothed over 200 ms in time.
- Duplicates
- Two probes can both be called hdri_soft - Blender's .001 suffix keeps their captures distinct.
How capture works
- First launch
- The HUD hides for a few seconds while the client visits each probe position and photographs all six directions. The faces land in data/maps/<name>/hdris/ and play resumes.
- Later launches
- The faces are read straight off disk. No capture pass, instant connect.
- Re-capturing
- Moved a probe, renamed one, or changed the world lighting? Delete data/maps/<name>/hdris/*.jpg and launch again - the missing faces trigger a fresh pass.
- --capture-ibl
- Forces a capture pass even when the faces exist, then exits. This is the batch-script path. Interactive play relies on the automatic trigger.
- --capture-ibl-equirect
- With the above, also writes a 2048x1024 equirectangular preview per probe - the easiest way to eyeball what a probe actually saw.
- --debug-ibl
- Spawns a chrome ball a metre in front of the camera that reflects the active probe. Sky on top, ground below, geometry around the sides means it is working.
A grey or part-black ball means one of three things: the map has no hdri_* marker at all, the player is in a zone probe's box with no hdri_default to fall back to, or a capture was interrupted - delete the JPEGs and relaunch.
What survives the trip through glTF.
Maps travel as glTF, and glTF is narrow. Knowing exactly what it keeps saves you from materials that look right in Blender and flat in game.
Maps travel as glTF, and glTF understands one thing: a Principled BSDF with image textures on its inputs. Build materials that way and they arrive intact. Anything cleverer is dropped silently, which is the one place this pipeline can waste your afternoon.
Comes through
- Principled BSDF with constant Base Color, Metallic, Roughness and Normal.
- Image Texture nodes wired straight into those inputs - albedo in sRGB, roughness and metallic as Non-Color, normal through a Normal Map node.
- One UV map per mesh (the active one).
- Alpha blend and alpha clip through the base colour's alpha channel.
Does not
- Procedural nodes - Noise, Voronoi, Checker, Brick, Gradient, ColorRamp and Math chains, Bump, Displacement. Bake them to an image first (Shader → Bake).
- Several BSDFs mixed together. One Principled BSDF per material.
- Node groups wrapping any of the above.
- Geometry, Tangent, Texture Coordinate, Light Path and AO node tricks.
Rule of thumb: reduce a material to "Principled BSDF plus image textures" in the Material Preview viewport. If it still looks the same, it will export the same. Whatever changed is what glTF was going to throw away.
Leave Emission Color black and Emission Strength at 0 on walls, floors and props. Emission is added on top of lighting rather than shaded by it, so an emissive wall comes out as a flat uniform colour. If something is too dark, brighten its base colour.
Seeing metallic and roughness for real
A grid of PBR spheres sweeping both controls ships with the game - metal along the top, plastic along the bottom, mirror-smooth on the left to fully matte on the right. Set your own values by matching them.
./client --test-scenario material_showcase --exit-after 10Thirteen tags, two sound banks each.
Tag a surface and it gets its own bullet impacts and its own footsteps. In a game where sound is information, this is not decoration.
Prefix a mesh with material_<tag>_ and it drives two separate sound banks: what a bullet sounds like hitting it, and what a player sounds like walking on it. Anything after the tag is yours - material_wood_floor_02 is fine. Untagged meshes fall back to concrete and log a warning at load so you can find them.
concrete
The default. Hard sharp thwack, crisp footsteps. Poured floors, rough walls, sidewalks, stone rubble.
dirt
Soft low thud, earthy footsteps. Ground, dry paths, flower beds.
wood
Splinter crack, hollow plank footsteps. Planks, pallets, doors, tables, crates.
glass
Crystalline shatter, brittle crunch underfoot. Windows, display cases, bottles.
metal
Ringing ping with sustain, heavy deck footsteps. Armour plate, I-beams, thick lockers.
steel
Thin sheet - brighter and ringier than metal. Catwalks, grates, ducts, panels.
tile
Ceramic: sharp crack, smooth footsteps. Bathrooms, kitchens, indoor corridors.
carpet
Muffled to near-silent. Rugs, carpeted rooms, soft furniture, foam mats.
sand
Gritty shuffle. Beaches, deserts, sandbag tops, bunker floors.
mud
Wet squelch. Swamps, rain-soaked trenches, river banks.
grass
Soft rustle. Lawns, meadows, groundcover.
foliage
Brush and leaves for impacts. Footsteps fall back to grass, because you walk on the ground under a bush rather than on the bush.
cardboard
Hollow crumple. Boxes, packaging, thin partitions.
The tag has to match the whole token: material_metal_* is metal, but material_metallic_*, material_sandbag_* and material_gravel_* are all untagged. Case is ignored and .001 suffixes are stripped.
The example map parks a row of thirteen colour-coded cubes, one per tag, between red spawn and the kit row. Walk on each to hear its footsteps, shoot each to hear its impacts.
Realistic environment sounds.
A named cube is a zone of looping sound at a stated loudness with a stated falloff. One marker plays everywhere as the bed.
A mesh cube named ambience_<db>_<easing>_<stem> becomes a zone of looping sound. The cube's box is the zone, the cube itself is deleted at load, and the audio comes from data/sounds/ambience/<stem>.ogg.
ambience_-40_soft_rain rain.ogg at -40 dBFS, smoothstep edges
ambience_-24_hard_generator generator.ogg, strong core, sharp boundary
ambience_-45_default_wind wind.ogg everywhere on the map (exactly one)- <db>
- A signed target loudness in dBFS between -80 and 0. The source file is normalised to hit it, so how hot the .ogg was cut does not matter. Ambience belongs around -35 to -50: it is a bed under the game, not part of it.
- <easing>
- soft is a smoothstep, medium is linear, hard is quadratic, instant is binary. default means "play everywhere" - exactly one marker per map carries it, and zones fade against it.
- <stem>
- Everything after the third underscore, underscores included. A missing .ogg is a hard error at load rather than a silent map.
Walking through a doorway crossfades the zone against the map-wide default over a couple of hundred milliseconds, so there is no click and no sudden swap.
Plants that move at their own scale.
Three presets, chosen by name, driven by a vertex shader. Grass flutters, bushes sway, trees roll.
Three presets, picked by name, sway a mesh in the wind. The frequencies differ because real plants differ: a half-metre blade of grass flutters far faster than a five-metre tree.
- movement_grass_*
- About 2.4 Hz, the whole blade moving, 0.35 m of wave height. Grass tufts, wheat, reeds.
- movement_foliage_*
- About 1.4 Hz with medium tip emphasis and 1 m of wave height. Bushes, ivy, hedges.
- movement_tree_*
- About 0.7 Hz with a steep mask, so the canopy moves and the trunk stays put. 4 m of wave height.
Modelling rules
- Origin at the base
- The shader uses local height as the sway mask: zero at the origin, full amplitude at the wave height. A mesh whose origin is not planted will sway around the wrong pivot.
- Two-sided already
- Backface culling is off for these, so single-plane grass cards and flat leaf cards work without duplicating and flipping them.
- Share materials deliberately
- Your Blender material assignments are kept as-is, and the exporter only prefixes their names. Ten thousand grass tufts sharing one material collapse into one draw call, and a hundred separate materials are a hundred draws. Share for fields, split for variety.
- Not baked
- Swaying meshes are excluded from the lightmap and lit by sun and ambient only - a static bake cannot represent moving geometry.
Your own materials, reloaded as you save.
Any mesh can be routed to a shader file, and shader files recompile while the game is running.
Name a mesh with shader_<kind>_<stem> and it renders through <stem>.ron instead of the default map material. Write your own and it works exactly like the shipped ones.
Pick a kind
The kind says what the mesh does in the lighting bake, and it is required. A bare shader_<stem> is refused at export rather than guessed at, because the three behave very differently and the wrong one is hard to spot later.
- shader_bake_<stem>
- Takes part in the bake like any normal surface and casts a baked shadow. For a shader that only changes how a static surface looks.
- shader_dynamic_<stem>
- Kept out of the bake, but still casts a real-time shadow. For something that moves or animates and should still darken the floor under it.
- shader_no_shadow_<stem>
- Kept out of the bake and casts no shadow at all. For sky domes, water and anything a shadow would look wrong on.
What ships
data/shaders/map_default.ron the default for every map mesh
data/shaders/glass.ron translucent glass
data/shaders/glass_window.ron breakable window pane
data/shaders/water.ron still water
data/shaders/ocean1.ron animated water
data/shaders/sky1.ron animated sky-dome gradient
data/shaders/hologram.ron animated scanline hologram
data/shaders/fluorescent_tube.ron strip-light tube
data/shaders/rim.ron rim-light highlight
data/shaders/toon.ron flat-shaded cel look
data/shaders/toon_outline.ron cel look with an outlineWhere to put yours
A shader inside your map's own folder is found first, so put it there and the map stays one self-contained thing to hand to somebody. A shader in the shared folder is available to every map.
data/maps/dusty_heron/shaders/<stem>.ron your map's own - searched first
data/shaders/<stem>.ron shipped with the gameBecause your folder wins, two maps can both ship a shader called water without either one clobbering the other - and a map can override a shipped stem, map_default included, for its own geometry only.
Hot reload
- File watcher
- Every save to a .ron recompiles in place while the game keeps running - the shared folder and your map's folder both. A line in the terminal confirms each reload, naming the file it compiled.
- F10
- Recompiles every shader the scene is using, in under a millisecond. Use it when your editor's save produces filesystem events the watcher misses.
- Errors
- A parse or compile error stops the game with the message and the file, rather than quietly keeping the last shader that worked. A missing shader names every folder it looked in. Fix, save, and the next reload picks it up.
For pure shader work, host the example map with the bake skipped - a clean stage that loads in seconds: ./client --host-map data/maps/_example_map/map.blend --skip-bake
Play it with bots to test it.
A map is a set of timings and angles. The only way to find out what yours are is to play a real match in it.
Walking a map alone tells you almost nothing. Bots fill it out: they path, they take angles, they trade, and they will find the sightline you did not know you built.
./client --host-map data/maps/de_house/map.blend --skip-bake --host-bots 5- --host-bots 1
- A 1v1 duel - quickest way to test one angle.
- --host-bots 3
- 2v2.
- --host-bots 5
- 3v3.
- --host-bots 9
- A full 5v5 competitive match.
Bots split evenly across the two teams, each runs in its own muted background window, and they all shut down when you close yours. Add --host-port 27016 to run a second copy alongside the first.
What to look for
- Spawn to site
- Time both teams to both sites. A site that one team reaches ten seconds earlier is not a site, it is a gift.
- Both halves
- Sides swap at halftime, so every angle has to be fair from both ends.
- Sound
- Stand still and listen. Footsteps should tell you where someone is. If a room is silent or every wall sounds identical, check your surface tags.
- Falling
- Try to leave the map. Anywhere you can reach that you should not either needs geometry or a kill_zone.
The whole command line, in one place.
Everything above, condensed.
The four you will actually type
Everything below this is reference. These are the commands a working session is made of.
- Get in fast, no bake
- ./client --host-map data/maps/dusty_heron/map.blend --skip-bake - seconds to load, sun and ambient only. This is the one you run all day while you move walls and place spawns.
- See the lighting
- Same command with --fast-bake instead. One Cycles sample: noisy, but it shows you where the light lands without the wait.
- Bake it properly
- Drop the flag entirely: ./client --host-map data/maps/dusty_heron/map.blend. Minutes the first time, instant afterwards. Run it once the geometry has settled and before anyone else loads the map.
- Play it with bots
- Add --host-bots 9 to any of the above for a full 5v5 to walk the sightlines with.
Nothing changed but you want the bake redone anyway? --force-export ignores the cache for one run.
Everything else
Client
connect to a server
--host-map host a .blend or .glb and connect
--skip-bake no Cycles bake
--fast-bake 1-sample bake
--no-bake-gpu bake on CPU
--bake-workers worker count
--force-export ignore the bake cache
--capture-ibl force a probe pass, then exit
--capture-ibl-equirect also write equirect previews
--debug-ibl chrome ball in front of the camera
--check-map check a .blend and exit, no bake
--host-bots bots alongside --host-map
--host-port embedded server port
--host-tickrate embedded server sim rate: 32, 48,
64, 96, 128 or 192 (default 96)
--name display name
--list-scenarios list built-in test scenarios Server
./server start on 27015
--port listen elsewhere
--map start on a specific map
--team-size players per team
--tickrate sim rate: 32, 48, 64, 96,
128 or 192 (default 96)
--name name in server browsers Handy scenarios
--test-scenario material_showcase --exit-after 10
--list-scenariosThe built-in scenarios are the same ones the game is tested with - scripted input through the ordinary input path, so what you watch is what a player would get.
What the messages mean.
The pipeline stops loudly instead of shipping a map that is quietly broken. These are the ones you are most likely to see.
The pipeline stops loudly rather than shipping a map that is quietly wrong, so most of what goes wrong announces itself. Here is what the common ones mean.
- "no bomb_zone_red mesh"
- Bomb sites are named bomb_zone_red and bomb_zone_blue, those exact names. A zone called bomb_zone_a is ignored, and a map with no zones never leaves the buy phase.
- "floor is N m below it"
- A spawn marker is hanging in the air. The message gives you the Z to set it to.
- "no ambience_<db>_default_<stem> marker"
- Every map needs exactly one map-wide ambient sound. Add a cube named something like ambience_-45_default_wind, using a stem that exists in data/sounds/ambience/.
- "textures are not packed"
- File → External Data → Pack Resources, then save the .blend.
- "bare shader_"
- A shader mesh needs a kind: shader_bake_, shader_dynamic_ or shader_no_shadow_ before the stem.
- Mesh is invisible in game
- Almost always a material problem: a mesh with no material at all, or UVs too broken to shade. Both are reported by --check-map. A mesh you can shoot but not see is this.
- Material looks flat or plain
- Maps travel as glTF, which only understands a Principled BSDF with image textures. Procedural nodes are dropped. Bake them to an image first.
- Surface looks washed out
- Emission. It is added on top of lighting instead of being shaded by it, so an emissive wall comes out a flat uniform colour. Leave Emission Strength at 0 on ordinary surfaces.
- Chrome reflects grey
- Either the map has no hdri_default probe, or a probe capture was interrupted. Delete data/maps/<name>/hdris/ and launch again.
- Lighting did not change
- A baked map is cached against the .blend. Save the .blend, or pass --force-export to rebake regardless.
- Blender not found
- --host-map and --check-map both need Blender 4 or 5 on your PATH. A map that is already baked to .glb needs no Blender at all.
Anything not on this list: the message names the object and the file it came from. Bring it to the Discord with the map and it will get answered.
Made something? Post it in the Discord.
There is no submission form and no approval queue. Drop your map in the Discord and, if it plays, it goes into the game and onto the official servers.
What to send
Pack your textures into the .blend (File → External Data → Pack Resources) and send that one file, plus any custom shaders you wrote. Everything else rebuilds from it. Zipping the whole map folder and sending that is just as good if it is easier, it is only bigger.
The zip is what you want anyway if you are sending it to a friend to play before it ships, because then they need your baked .glb, lightmap and hdris/ too. Everyone in a match has to have byte-identical map files, and two people baking the same .blend do not get byte-identical results. Send the bake, not the recipe.
What happens next
I bake it, play a bot match in it, and send back anything that needs a look - a spawn that falls, a site nobody can hold. Then it ships with the game and goes into the map vote on the official servers, under your name.
Half-finished is fine. A greybox with spawns and two sites is enough to play, and playing it is how you find out whether it is worth finishing.