DOMI-NATION Map Authoring Guide
The definitive start-to-finish guide to building, validating, and shipping a DOMI-NATION skirmish or campaign map with the in-editor map tool. Follow it top to bottom and a cold reader ships a valid 1v1.
- Editor:
Tools → DOMI-NATION → Map Editor(DomiNationMapEditorWindow). - Schema (source of truth):
unity/Assets/Scripts/Map/MapDescriptor.cs. - Load order + engine rules:
unity/Assets/Scripts/Map/MapLoader.cs. - Validator (every rule):
unity/Assets/Scripts/Map/MapValidation.cs. - Multiplayer packaging/approval:
docs/custom-maps.md.
A map is one pure-data JSON file (MapDescriptor) in StreamingAssets/data/maps/<id>.json
(built-ins), data/custom_maps/ (project custom), or persistentDataPath/maps/ (user).
No scene edits — the descriptor is portable and diffable.
0. The mental model (read this first)
The runtime builds a map in a fixed order (MapLoader). Understanding it prevents 90%
of authoring mistakes:
- Generator runs —
GridMap.GenerateFromDescriptorproduces a base grid frombiomeseed+generatorParams. Forgenerator: "none"this is a flat canvas.
- Tile overlay —
ApplyTileOverlay: the authoredterrainRowsgrid is stamped over the generated base, then any sparseoverlayTiles/terrainLayers. - Resource overlay —
ApplyResourceOverlay:oreblobs stamp walkable ore ground;derricknodes are recorded for the world-build pass. - Elevation sculpt LAST —
elevOffsetsper-cell height deltas are added on top of the derived elevation. Because this runs last, every downstream reader (render, pathing, minimap) inherits the sculpt from one place.
The single most important fact: terrainRows is a full h×w grid that OVERWRITES the
generator. When a map ships terrainRows, the water/road/bridge/rock you see is authored
there — not in overlayTiles, and not left to PerlinNoise. A JSON census that scans only
overlayTiles will report "zero authored water" and be wrong (this exact mistake was
corrected in the 1.0 director audit). If you author terrain, author it in terrainRows.
Two water systems coexist, and runtime truth is always map.tiles after MapLoader finishes:
- Tile water — deep
wand wadeable shallowhcells interrainRows/overlayTiles, or a generator's river. Exact, deterministic, no noise. Both share one visible water surface;hexposes a lighter submerged shelf and excludes ships/whitecaps while remaining wadeable by infantry and amphibious units. - Elevation-carved water — the River Carve brush writes
wtiles and feathers banks viaelevOffsets. Still deterministic.
1. Create the map
Toolbar → New, or seed from the Generation Presets foldout.
Production skirmish sizes are exact and square:
| tier | dimensions |
|---|---|
| SMALL | 184 x 184 |
| MEDIUM | 256 x 256 |
| LARGE | 384 x 384 |
| XL | 512 x 512 |
| XXL / MASSIVE | 1024 x 1024 |
The editor's Resize tier control offers only these values. New and redesigned skirmish maps use their tier exactly; arbitrary and rectangular skirmish dimensions are retired. Campaign maps, tutorials, and small logic fixtures are exempt until separately scheduled. Never shrink a tier to work around an engine limit; fix and measure the engine instead.
A. Blank canvas (MapSaveLoadService.CreateNew): the production UI defaults to SMALL
184 x 184 and supports every tier through 1024 x 1024. The low-level API still accepts smaller
arbitrary grids for focused editor tests. Biome defaults to wilderness; generator: "none";
seed: 20260630 (fixed and deterministic). You get two starts (p1, p2), two ore blobs,
one central derrick, and a full grass terrainRows grid to paint on.
B. Generation Preset (MapGenerationPresetCatalog) — a first-pass, editor-polish-ready
map with terrain, roads, spawns, resources, strategic points, AI zones, waypoint paths, and
production stamps already placed. 13 presets:
| id | tier / size | players | biome | layout | naval |
|---|---|---|---|---|---|
balanced_duel |
SMALL 184 | 2 | wilderness | duel_crossing | – |
team_war_2v2 |
MEDIUM 256 | 4 | wilderness | lane_based | – |
large_battlefield_3v3 |
LARGE 384 | 6 | wilderness | radial_ffa | – |
ffa_4player |
MEDIUM 256 | 4 | wilderness | radial_ffa | – |
chaos_6player |
LARGE 384 | 6 | volcanic | radial_ffa | – |
naval_island_war |
MEDIUM 256 | 4 | islands | island_clusters | ✓ |
river_crossing |
SMALL 184 | 2 | wilderness | river_divided | – |
urban_combat |
SMALL 184 | 2 | city | urban_grid | – |
mountain_pass |
SMALL 184 | 2 | wilderness | mountain_pass | – |
desert_oil_war |
MEDIUM 256 | 4 | desert | open_battlefield | – |
farmland_conflict |
SMALL 184 | 2 | wilderness | semi_symmetrical | – |
industrial_zone |
MEDIUM 256 | 4 | city | lane_based | – |
coastal_assault |
MEDIUM 256 | 4 | lake | center_contest | ✓ |
Optional Theme and Layout overrides ((preset) = keep). "Create First Pass" builds
in-memory; "Create, Save, Validate" also writes + runs the validator. The seed is stable per
id:presetId, so regenerating the same map reproduces it byte-for-byte.
Metadata to set now (Map Settings / Environment foldout): name, author,
description, recommendedPlayers (1–16). id is sanitized from the name (lowercase,
[a-z0-9], _-collapsed) and MUST match the filename.
World Creator Professional is excluded from this project. DOMI-NATION does not use Unity
Terrain heightmaps; use deterministic terrainRows, elevOffsets, and the native
hill/mountain/ridge/cliff/plateau/valley/pass tools.
2. Terrain
Two ways to define ground; prefer terrainRows for authored maps.
- Generator (
generator: "wilderness"etc., noterrainRows): terrain is computed from biome + seed. Rivers/lakes come from the heightmap. You get variety for free but cannot see exact cells in the JSON — you must Validate to catch a start or building the generator dropped on water. - Authored
terrainRows(generator: "none", full h×w grid): every cell is explicit. Exact, diffable, and the editor's preferred save format. This is what every polished skirmish map ships.
Paint with the Terrain tool. Brush shapes: Brush (round, size 1–9), Line (press-drag-release Bresenham with brush width), Rect (filled/outline), Fill (flood-fill). Eraser resets to the biome base (sand on desert, grass elsewhere). Right-click samples the terrain under the cursor. Feather Biome Edges softens hard biome borders.
Terrain character map
terrainRows uses one char per cell. The canonical map (MapSaveLoadService.TerrainCharMap;
data/terrain.json can add more):
| char | id | build | pass | speed | use |
|---|---|---|---|---|---|
g |
grass | ✓ | ✓ | 1.0 | biome base |
d |
dirt | ✓ | ✓ | 1.0 | biome base |
s |
sand | ✓ | ✓ | 1.0 | desert base, shorelines |
k |
rock | ✗ | ✓* | 1.0 | cliffs — +4.5 elev → impassable (see §3) |
r |
road | ✓ | ✓ | 1.5 | map-style road; auto junctions by adjacency |
a |
paved_road | ✓ | ✓ | 1.5 | asphalt avenue (curbs/lane lines), any biome |
q |
concrete_road | ✓ | ✓ | 1.5 | concrete slab / military route, any biome |
w |
water | ✗ | ✗ | — | naval-only; ground blocked |
h |
shallow | ✗ | ✗† | 0.6 | visible infantry/amphibious ford; vehicles/ships blocked |
b |
bridge | ✓ | ✓ | — | the ONLY ground crossing over water |
o |
ore | ✗ | ✓ | — | harvestable (usually from resourceNodes, not painted) |
c |
concrete | ✓ | ✓ | 1.25 | base pads, industrial yards |
p |
pavement | ✓ | ✓ | 1.4 | bridge approaches, aprons, parking |
m |
mud | ✗ | ✓ | 0.65 | river banks, swamp — slows pushes |
i |
ice | ✗ | ✓ | 0.85 | frozen crossing: ground units walk it, ships blocked |
x |
scorched | ✓ | ✓ | 1.0 | battle damage, burn scars, wrecks |
* rock is passable by the terrain.json flag but impassable in practice because
ApplyTile bumps its elevation +4.5, over the MaxWalkElev 3.5 walk ceiling. Rock is the
correct tool for an intentional un-climbable wall — do not use the passable flag for walls.
† shallow is not generally passable: movement-domain rules admit infantry and amphibious units only.
Ordinary ground vehicles and naval units refuse it.
Note ice is passable to ground but water-blocked, so ice fords double as land routes on
frozen maps; ships still need open w.
3. Elevation (elevOffsets)
Authored height sculpting layered over the derived biome/rock/water elevation. Sparse RLE string, one entry per non-zero cell:
"gx,gy,dh;gx,gy,dh;..."
dh is an additive float delta (parsed by MapLoader.ParseElevOffsets; malformed
entries are silently skipped). Applied LAST and additively, so a later duplicate of the
same cell stacks. Absent/empty = no sculpt = byte-identical to a legacy map. Deterministic
from map data → lockstep-safe.
New maps created by the editor and generation presets set
generatorParams.authoredFlatBase: true. Their authored canvas starts at elevation 0, so
elevOffsets is the exact relief instead of an offset on top of hidden biome noise. Legacy
maps leave the field false and retain their existing generated elevation.
The cliff rule (memorize this)
Walkability is an absolute cap, not a per-step climb limit:
A cell is walkable iff its final elevation
≤ GridMap.MaxWalkElev(3.5).
There is no slope/step check — units cross any height difference between two cells as long as both stay ≤ 3.5. The real hazard is an offset that raises walkable ground above 3.5, turning it into an accidental impassable wall units route around. Consequences:
- Guaranteed cliff iff
dh > 3.5on flat base ground. - Guaranteed walkable iff
base_max + dh ≤ 3.5. - Rock cliffs come from the +4.5 elevation bump (
ktile), not the passable flag. - The Elevation tool clamps offsets to ±4; keep tactical high-ground
≤ 3.5; usekfor walls. The validator warns on over-cap sculpted cells (see §11,CheckElevationSeams). - A sharp edge between two ordinary walkable cells is visually misleading because units can
cross it. The validator warns when that walkable seam exceeds
1.75; smooth it or mark the edge with an explicitkcliff contour.
Brushes (Elevation tool)
Raise/Lower (±strength 0.1–2, size 1–8), Flatten (pull footprint toward the hovered cell's height), Smooth (3×3 mean), Ramp (A→B) (click LOW end then HIGH end — writes a linear walkable slope, refused if it would sever the start-to-start path), River Carve (§4). The canvas tints raised cells red / lowered blue.
Native landform generation
Choose Elevation → Landform for deterministic authored forms:
- Hill: one-click smooth, fully walkable dome.
- Mountain: one-click rugged foothills with an explicit rock crown.
- Plateau: one-click flat tactical top with a rock escarpment ring. Roads remain openings.
- Ridge: two-click elongated walkable high ground.
- Cliff: two-click directed escarpment.
A→Bis the rock edge; its left side is raised. - Valley: two-click lowered corridor, clamped above the visual water plane.
- Pass: two-click mountain ridge with a wide center gap and infrastructure openings.
Height, radius/half-width, roughness, and seed are authored controls. Enable Preserve bases, resources, objectives, roads, and rail passes for production work. The apply is refused and fully restored if the candidate disconnects player starts.
The same panel offers full-map templates: flat, rolling_hills, highlands,
mountain_chain, plateaus, canyon, coastal_bluffs, and mixed. Templates use
proportional paired features, record their seed/style in generatorParams, bake exact
elevOffsets, preserve gameplay pads, and remain editable with every other elevation brush.
Generation Presets expose the same Relief selector, so generated first passes contain real
terrain forms instead of terrain-character-only rock stripes.
Shipped recipes (measured, offline BFS-verified)
- Twin-ridge divide —
frontier_divide(256×192, wilderness). Two 5-wide walls atdh = 5.0(well over the 3.5 cap → guaranteed cliffs), each pierced by 3 staggered 13-wide passes. All 8 spawns / 16 ore / 10 derricks stay BFS-connected through the passes. This is the canonical "ridge with gaps" pattern: build the wall over-cap, then carve wide passes back under-cap. ItselevOffsetsstring is long (~14 KB) precisely because it is a dense wall of...,5entries. - Central walkable hill + rocky crown —
high_command(256×184). A smooth walkable dome (offsets ramp0.2 → ~0.95, all under cap) with a rock (k) crown for the un-passable peak and a 4-garrison town at the foot. Uses sub-1.0 offsets to shape readable high ground you can still fight on. - Mesas —
sandstorm_flats(WEAK, learn from it):dh = 7.0mesas parked in opposite corners create no corridor pinch — high ground with no tactical purpose. Lesson: elevation must gate a route or command a contested lane, not decorate a corner.
4. Water + River Carve
- Painted water: paint deep
wor wadeable shallowhinterrainRows(Terrain tool). Usehonly where ground traversal is intentional. River profiles are opaque, so their submerged gameplay shelf is deliberately hidden; naval units and open-water whitecaps still excludeh. Addm(mud) ors(sand) banks for readable shorelines — the generator'sPaintBanksdoes this automatically on preset maps. - River Carve brush (Elevation tool → River Carve): drag to carve a river — the core
band becomes
waterand the banks are feathered to an intermediate height viaelevOffsetsfor a soft shoreline. River Width 1–8 tiles, feathered edges. Undo-able. Span it with the Bridge tool.
Water profiles and currents
The runtime editor's left palette selects a map-level water profile and previews it immediately.
Dir -15/+15 rotates the presentation-only heading: prevailing wind for ocean/coastal/lake profiles,
and downstream current direction for rivers/marsh. Speed -/+ adjusts the 0..2.5 propagation multiplier.
Motion -/+ adjusts waterMotionScale from still (0) through the profile default (1) to the supported
dramatic maximum (2). The renderer derives a compact three-band gravity-wave spectrum from those values;
river bands remain aligned downstream, while open-water bands spread around the prevailing wind. These
values save with waterProfile, waterFlowAngle, and waterFlowSpeed. They never affect pathing, naval
steering, projectiles, terrain checksums, or lockstep simulation.
| Profile | Visual identity | Motion / shore treatment |
|---|---|---|
ocean |
Deep blue, stronger horizon response | Crossed moving swell faces, broken crest arcs, full travelling surf |
coastal |
Brighter blue-teal shallows | Moderate swell, broken crests, and coastal wash |
lake |
Calm blue-green, nearly opaque | Small moving ripples, almost no cresting, gentle shore wash |
clear_river |
Dark natural alpine teal, opaque | Continuous flow lanes, downstream ripple packets, restrained bank wash |
silt_river |
Lighter tannin/sediment brown, opaque | Softer downstream flow lanes and packets, warm low bank wash |
marsh |
Dark olive-green, opaque | Near-still slow drift and a quiet moving bank edge |
If waterProfile is absent, legacy maps infer a profile from biome: islands → coastal, lake → lake,
wilderness → clear river, city/desert → silt river, and other liquid maps → ocean. Snow and volcanic
maps continue to use their dedicated ice and lava materials.
Every water body that separates two player starts must be crossable (bridge or ice ford), or the connectivity validator errors. On water maps the validator also requires two vertex-disjoint ground routes between each spawn pair (§11) so one lost bridge can't cleave the map.
5. Roads (the network tool)
Road tool: pick a type — map-style (r, dirt/asphalt/concrete by biome or
roadSurface), asphalt avenue (a), or concrete slab (q) — and a width (1–2),
then drag to lay road along the mouse path (fast drags stay continuous). The renderer picks
straights / turns / T-junctions / crossings / plazas from adjacency automatically at render
time (real HD Roads tile atlas — the editor canvas matches the shipped 3D look; toggle Road
Tex). Water cells under the drag become bridge automatically, so dragging across a
river lays the crossing. Right-click erases (road → biome base, bridge → water). Roads
never pave over ore.
Design intent: connect every start to the contested center and to its expansions. A road that dead-ends on dry land away from the map edge is flagged (§11) — end roads at a destination (base, building, resource, objective), at the map edge, or at a shoreline.
6. Bridges
Bridge tool: click one bank (anchor), then the far bank — the tool validates and stamps a clean rectangular deck across the water plus a short road approach onto each bank.
Span limits (MapAuthoringTools, hard-validated before stamping):
BridgeMinSpan = 3— the span must be length ≥ 3 (and must actually cross water).BridgeMaxWidth = 6— deck width ≤ 6. The tool's Deck Width slider is 2–3 (keep crossings 2–3 wide; a deck under 3 tiles funnels units single-file).- Both ends must land on buildable land.
- Each deck is its own rectangle. Touching an existing deck only warns — merged bridge
clusters render as the degenerate per-tile procedural fallback instead of the modular
BridgeSet crossing (§11,
CheckBridgeClusterGeometry). Stamp bridges as clean filled rectangles.
Lifecycle context (#1919): at runtime GameController.BuildBridges reads each bridge
cluster; a clean filled rectangle (fill ≥ 50% of its bounding box, 3 ≤ length, width ≤ 6)
gets the modular BridgeSet model — anything else falls back to the per-tile deck. Prefer
Assets/BridgeSet models; procedural BridgeSegment is the fallback. EasyRoads3D Pro
needs a driver-approved plan (it targets Unity Terrain, not our custom ground mesh).
7. Spawns, resources, strategic points (fairness)
Start tool: place player starts p1..pN. Requirements: ≥ 2 starts and
≥ recommendedPlayers. The prefix az_ is reserved for campaign wave anchors (amphibious
/ scripted) and is filtered out of skirmish starts and the ≥2 count. A p* start
force-spawns the base HQ unflattened (no CanPlace gate, no footprint flatten), so it MUST
sit on buildable land — water floats it, rock clips it, and the base can't expand.
Per-start metadata (selected-start inspector): Team (0 = FFA/auto; same number = allied), Starting Credits (0 = match setting), Facing (deg 0–359, -1 = auto). Team/credits/ facing honoring in the sim is gated behind the multiplayer-sync pass; the fields are authored-and-validated now.
Resource tool:
ore— a harvestable blob, half-extentsrx/ry(a2×2half-extent = 5×5 blob).ApplyResourceOverlaylevels ore to walkable ground even over rock/water.derrick— a capturable oil derrick (a point; no tile stamp). Must be ground-reachable from a start or it errors (#1939 shipped 7 land-locked derricks before this rule).- Strategic points (
StrategicPoint.type):garrison,repair,tech,tech_objective,derrick,barrels,fuel_depot. Put afuel_depot(6 barrels) orbarrels(4) at a chokepoint for a classic explosive hazard — keep it > 13 cells from every spawn (the engine placer skips closer cells) and verify its 3×3 block doesn't sever a lane.
Fairness norms (per-spawn nearest-node Euclidean distance; enforced + audited):
- Every start needs ore within 34 tiles (else warning).
- Nearest-ore spread across starts must be ≤ 10 tiles (else "uneven resource access").
- Mirror resource value across starts on competitive maps. The
PER_MAP_DELTA_AUDITtarget is a fairness gap ≤ 13; strong maps sit near 0. - Two starts < 24 tiles apart warns; starts within 10 tiles of a map edge warn.
8. Props (grounded catalog)
Object tool: place buildings/defenses/units by faction def, or catalog props via the
asset picker (grouped, biome-filtered, thumbnailed, searchable — backed by
MapEditorAssetCatalog). Every catalog key is auto-baked into the player build
(BakeModelDB → AllModelKeys()), so adding a catalog entry is the complete wiring.
Scatter Brush (prop/tree/rock): density-paints — radius 2–12, 1–20 per click, random rotation, ±20% size jitter, one per cell, skips water/roads/ore/occupied cells. Enable Biome Foliage to draw random grounded keys from the biome palette (spruce on snow, palms on islands — never palms on ice). Select tool box-selects (Shift = additive) for Ctrl+C/V copy-paste at the cursor and Del/Ctrl+D.
Banned keys — hard ERROR at save + CI. The Polytope medieval-fantasy dock series
(prefab_polytope_dock_*) and recolored grass tufts (Grass_Tall_Orange/White, Grass_Sea)
are rejected (MapEditorAssetCatalog.IsBannedModelKey). Swap for grounded catalog keys
(containers, gas tank, cranes, patrol boats, real trees/grass). Art direction: clean low-poly
semi-realistic — no cartoon *_Prefeb keys, no photoreal.
Density / variety norms (PER_MAP_DELTA_AUDIT; density = props per 10 000 tiles):
- ≥ 10 props/10k tiles — below that a skirmish map reads bare (validator warns under 10 total placed objects; tutorial maps exempt).
- ≥ 12 distinct model keys — low variety (7–9 keys) reads repetitive.
- Fairness gap ≤ 13 (per §7).
Spawn clearance: keep scatter and blocking props ~8 tiles clear of every start. The
scatter brush avoids water/roads/ore but does not auto-clear the base footprint — a
blocksPathing prop inside a start's ~10×8 HQ pad + build halo cripples base buildout. Keep
blocking props out of contested lanes narrower than ~4 cells; deep inside a 5×5+ road plaza is
fine. A land prop dropped on open water warns as "floating."
9. Zones, waypoints, triggers (campaign)
- Waypoint tool → named points (
WaypointData); chain them into waypoint paths (WaypointPathData,domain: ground|naval|air,patrolflag) for attacks/patrols/ reinforcements. Every path needs an id and ≥ 2 waypoints, and each referenced waypoint must exist. - Zone tool → AI base/build/defensive/expansion zones (
AIMapData) and campaign trigger areas (MapZone, circular{id,x,y,radius}). Convention:zone_*= trigger areas,wp_*= escort waypoints. Custom maps warn if they have no AI base zones or no attack paths. Zones withradius ≤ 0warn. - Triggers (
TriggerData): conditions + actions,targetIdmust resolve to a real entity id (spawn/zone/strategic/placed/waypoint/path/trigger). A trigger with no conditions and no actions warns; a danglingtargetIderrors.
Skirmish maps leave these empty — the format is purely additive, so a map without
triggers/zones/waypoints deserializes exactly as before.
10. Weather + environment
Map Settings / Environment foldout:
- Weather (
weather):(biome default)/clear/rain/fog/snow/ash. Empty = biome-implicit (snow→snowfall, volcanic→ashfall). - Sight multipliers (
GameController.weatherSight, feeds FogOfWar reveal + unit acquisition): fog 0.70, rain/storm 0.80, everything else (clear/snow/ash) 1.0. Fog and rain literally shrink vision — a fog map rewards scouting and short sightlines; the HQ stays lit in any weather but forward structures see less. - Road surface (
roadSurface): auto (city→asphalt, else dirt) / dirt / asphalt / concrete. - Time of day (
timeOfDay): day / dawn / noon / dusk / night — repositions + retints the sun (night keeps lifted ambient for readability). - Lighting (
lighting):noon (default)/dawn/dusk/overcast/night— a curated preset that bundles sun angle+colour+intensity+shadow-strength, Trilight ambient, and a fog-density MULTIPLIER (it modulates the biome/weather fog, doesn't replace it).nightalso switches on warm emissive building-window glow (bloom-lit, no per-building lights). Empty = the noon default (unassigned maps are unchanged). SupersedestimeOfDaywhen both are set. Curated values live inAssets/Scripts/Engine/LightingPresets.cs. Presentation-only — never touches the sim (lockstep-safe). Shipped assignments: caldera_protocol/hellfire_peninsula = dusk, district_zero/midnight_sun = night, frozen_divide = dawn, ashfall_perimeter = overcast. - Fog density (
fogDensity): dial haze independently of weather (-1 = auto).
11. Validate — every rule
Toolbar → Validate (V), or headlessly -executeMethod Build.ValidateAllMaps. Results
list is clickable (focuses the offending cell). MapValidator.Validate runs the full
skirmish rule set; ValidatePlacementOnly runs the biome-agnostic subset for campaign maps.
The CI gate MapQualityAudit.CheckShippedMapQuality fails the build on any shipped-skirmish
error and on any terrain near-clone ≥ 90%.
Below is every rule, its exact message (abbreviated), what it means, and the fix. ~21 error rules + ~19 warning rules across 20 check functions.
Errors — must fix (block save-quality + CI)
| # | Message (abbrev.) | Means | Fix |
|---|---|---|---|
| E1 | Map id is missing / Map name is missing |
Empty identity field | Set id/name (id = filename). |
| E2 | terrainRows height does not match map height |
terrainRows.Length != h |
Resize rows to exactly h. |
| E3 | terrainRows[y] width does not match map width |
A row's length != w |
Pad/trim row y to w chars. |
| E4 | ... is outside map bounds |
A spawn/resource/strategic/placed/waypoint/zone cell is off-grid | Move it inside 0..w-1 / 0..h-1. |
| E5 | At least two player start positions are required |
< 2 p* starts |
Add starts until ≥ 2 and ≥ recommendedPlayers. |
| E6 | Duplicate <kind> id: X |
Two spawns/objects/waypoints/triggers share an id | Make ids unique. |
| E7 | Waypoint path 'P' references missing waypoint 'W' |
Path lists a non-existent waypoint | Add the waypoint or fix the ref. |
| E8 | AI <attack/patrol> path references missing waypoint path: P |
aiData names a path that doesn't exist |
Define the path or drop the ref. |
| E9 | Trigger 'T' references missing id 'X' |
Trigger condition/action targetId unresolved |
Point it at a real entity id. |
| E10 | Building 'B' is on non-buildable '<tile>' terrain |
Placed building fails CanPlace (water/rock) |
Move onto buildable land. |
| E11 | Placed building/unit references missing definition: D |
defId not in any faction roster |
Fix defId. |
| E12 | Unit placed on blocked terrain: D |
Unit on impassable/occupied cell | Move to passable ground. |
| E13 | Player start X is on '<tile>' terrain |
p* start on water/bridge/rock |
Move to buildable land (az_ exempt). |
| E14 | No ground path between starts A and B |
A* finds no route (connectivity) | Add a bridge/ford/land route. |
| E15 | Bridge at (x,y) is not connected to land on either end |
Floating bridge (0 land contacts) | Land both ends on buildable ground. |
| E16 | Placed object uses banned model key 'K' |
Polytope/recolored-grass cartoon key | Swap for a grounded catalog key. |
| E17 | Oil derrick at (x,y) is unreachable by ground from any player start |
Land-locked derrick | Connect by land/bridge or relocate. |
| E18 | Strategic point 'S' at (x,y) is unreachable by ground |
Land-locked capture point | Connect or relocate. |
| E19 | Map generation failed: ... |
Generator/overlay threw | Fix the descriptor field named in the exception. |
Warnings — fix or justify
| # | Message (abbrev.) | Means | Fix |
|---|---|---|---|
| W1 | Map is very small for RTS play |
w < 48 or h < 36 |
Enlarge (§13). |
| W2 | Start X is close to the map edge |
Edge distance < 10 |
Pull the base inward. |
| W3 | Recommended player count exceeds authored start locations |
recommendedPlayers > p* count |
Add starts or lower the count. |
| W4 | Starts A and B are very close (N tiles) |
< 24 tiles apart |
Space starts out. |
| W5 | Waypoint path has no id / ... fewer than two waypoints |
Malformed path | Give it an id + ≥ 2 waypoints. |
| W6 | Trigger 'T' has no conditions or actions / missing an id |
Empty/anonymous trigger | Fill it in or delete it. |
| W7 | overlayTiles use undefined terrain 'T' (N cells) |
Semantic overlay id not in terrain.json |
Define it (buildable:false, passable:false, color) in terrain.json. |
| W8 | Sculpted terrain at (x,y) rises to N, above the walkable cap (3.5) |
An elevOffsets cell became an accidental cliff |
Lower the sculpt ≤ 3.5, or paint k for an intentional wall. |
| W9 | Terrain layer is N% identical to shipped map 'M' |
Near-duplicate (≥ 90%) of another shipped map |
Rework the terrain or retire one. |
| W10 | Snow-biome map has N liquid water cells but zero ice tiles |
Frozen map with open water, no ice | Paint ice sheets/fords at crossings. |
| W11 | Crossing at (x,y) narrows to N passable tile(s) |
Bridge/ice ford < 3 wide → single-file funnel |
Widen to ≥ 3 tiles. |
| W12 | Start X reaches no road tile and no other start |
Spawn cut off from the network | Connect with a road/passable route. |
| W13 | Water map has only a SINGLE ground route between starts A and B |
One choke; a lost bridge splits the map | Add a second vertex-disjoint route. |
| W14 | Only N placed object(s) — under 10 reads bare |
Prop density floor | Dress the battlefield (§8). |
| W15 | Bridge cluster at (x,y) fills only .../ is NxM |
Degenerate deck → per-tile procedural fallback | Stamp a clean filled rectangle (Bridge tool). |
| W16 | Start X has no authored resource within 34 tiles |
No ore/derrick near a base | Add a resource in range. |
| W17 | Uneven resource access: start X is N tiles ... but Y is only M |
Ore spread > 10 tiles between starts |
Balance nearest-ore distances. |
| W18 | Road dead-ends at (x,y) |
Road stub on dry land, ≤ 1 road neighbor, not at a destination/edge/shore |
Extend it or end it at a destination. |
| W19 | Bridge at (x,y) dead-ends into water |
Only one land approach | Land the far end. |
| W20 | Placed prop references a missing model key: K / negative target size |
Bad prop reference | Fix the key / targetSize. |
| W21 | Land prop 'K' sits on open water and will look like it's floating |
Terrestrial prop on w |
Move to land, or use a dock/ship/bridge asset. |
| W22 | Custom map has no AI base zones / no AI attack path hints |
AI has no authoring hints | Add base zones + attack paths. |
| W23 | AI <zone> 'Z' has no radius |
radius ≤ 0 |
Give the zone a radius. |
| W24 | Start X facing N° is out of range |
Facing ≥ 360 |
Use 0–359 or -1. |
| W25 | Team/credit sanity: lopsided (NvM) / no team while others assigned / all starts share one team / credits differ ... |
Spawn-metadata inconsistency | Make teams even, assign all or none, keep credits consistent per team. |
12. Preview bake
The skirmish map-select UI shows a baked top-down PNG with numbered spawn markers. In-editor:
Regenerate Minimap Preview. For the whole roster, driver-run offline
(SkirmishMapPreviewBaker.GenerateAllSkirmishPreviews) — one full BuildWorld per map, writes
Assets/Resources/MapPreviews/<id>.png (512-long-side), logs MAPPREVIEW markers, exits
nonzero on any failure:
ED=/home/jesse/Unity/Hub/Editor/6000.5.0f1/Editor/Unity
flatpak run --command="$ED" com.unity.UnityHub -batchmode -projectPath unity \
-executeMethod SkirmishMapPreviewBaker.GenerateAllSkirmishPreviews -logFile -
Do not pass -nographics (the capture pipeline SIGSEGVs on shadow maps without a GPU
context). The UI falls back to the live MinimapPreview schematic when a PNG is missing.
13. Offline connectivity proof + enlargement
Pessimistic-BFS connectivity proof (no Unity)
The 1.0 audit verified elevation maps offline with a pessimistic max-barrier BFS that needs no Unity build — reproduce it to prove a sculpt/terrain change keeps every spawn connected:
- Reconstruct the walk grid from the descriptor alone. Start from
terrainRows(w/b/k/osemantics per §2), then applyelevOffsetsadditively. A cell is walkable iff its tile is passable AND final elevation≤ 3.5(the cliff rule, §3). Because the authored region is deterministic (noPerlinNoisewhenterrainRowsis a full grid), the grid you reconstruct is exactly what the engine builds. - Be pessimistic on any noise-derived base: for a generator map, assume the maximum
base amplitude a cell could carry, so
guaranteed-walkable iff base_max + dh ≤ 3.5andguaranteed-cliff iff dh > 3.5. AuthoredterrainRowsmaps skip this — they're exact. - 4-connected BFS from
p1. Assert every otherp*start, everyore/derrick, and every strategic point is reached. 100% reached = connected. This mirrors the engine's ownMapValidator.CheckConnectivity(A* over the same blocked set), so a pass here predicts a pass in-editor.
Use blocksPathing props, startingState:"blocked", and category tree/rock as blockers
(BlockedWithPlacedObjects), exactly as the validator does.
Enlargement (+dx/+dy translate)
To grow a map without re-authoring (the high_command/tidal_basin +25% pattern): pick a new
w/h, pad terrainRows with base-terrain (g) on the new margins, and translate every
coordinate field by the same +dx/+dy so the authored region keeps its internal geometry —
spawnPoints, resourceNodes, strategicPoints, placedObjects, waypoints, zones,
every gx/gy in elevOffsets, aiData, and mapModules, and each x/y in zones. Then
byte-compare the authored (untranslated) region against the original to prove nothing moved
relative to itself, and re-run the BFS proof + Validate. Miss one coordinate list and you get a
silent desync between terrain and objects.
14. Driver gate checklist (ship it)
Docs-only and data-only changes still go through the verification ladder — pick the cheapest check that proves the change, in order:
- Save + Validate in-editor (
V) — zero errors; justify every warning. unity/compile-check.sh— if any C# changed (0 errors).unity/run-tests.sh— logic invariants (placement + elevation round-trip) after any logic/data change; exits nonzero on failure.-executeMethod Build.ValidateAllMaps— sweeps all ~48 maps → rankeddocs/map-validation-report.md(skirmish first, by severity). Re-run after a batch.-executeMethod MapQualityAudit.CheckShippedMapQuality— the CI gate (zero shipped errors, no≥ 90%terrain clone).- Preview bake (§12) if the map is new or its terrain/spawns changed.
- Overview capture —
Build.CaptureMapOverviewAuthored -mapId <id> -out <path>, or aunity/build-game.sh+ numberedunity/shots/NN_description.pngfor the visual audit trail — verify the shot shows the change. - QA-gate (
domination-qa-tester) before merge; branch + commit the instant edits exist (a concurrentgit reset --hardwipes uncommitted tracked edits).
Worked mini-example — a valid 1v1
A minimal authored wilderness 1v1, field shapes copied from a real shipped map
(proving_grounds). ASCII only; terrainRows abbreviated (it is a full h×w grid).
{
"id": "twin_ford",
"name": "Twin Ford",
"description": "Two river fords bracket a contested central town.",
"author": "DOMI-NATION Tools",
"recommendedPlayers": 2,
"biome": "wilderness",
"weather": "clear",
"roadSurface": "dirt",
"w": 120,
"h": 88,
"generator": "none",
"seed": 20260710,
"tags": ["skirmish", "1v1", "2p", "wilderness", "authored_terrain"],
"terrainRows": ["gggg...gggg", "gggg...wwww", "...(88 rows, each 120 chars)..."],
"elevOffsets": "58,40,4.0;58,41,4.0;59,40,4.0;62,40,1.2;62,41,1.2",
"spawnPoints": [
{ "id": "p1", "gx": 18, "gy": 66, "label": "South Base", "team": 1, "facing": -1.0 },
{ "id": "p2", "gx": 100, "gy": 22, "label": "North Base", "team": 2, "facing": -1.0 }
],
"resourceNodes": [
{ "type": "ore", "gx": 26, "gy": 58, "rx": 3, "ry": 3 },
{ "type": "ore", "gx": 94, "gy": 30, "rx": 3, "ry": 3 },
{ "type": "derrick", "gx": 60, "gy": 44 }
],
"strategicPoints": [
{ "id": "center_garrison", "type": "garrison", "gx": 60, "gy": 40, "label": "Central town garrison" }
],
"placedObjects": [
{ "id": "landmark_001", "category": "prop", "type": "landmark", "modelKey": "RadarStation",
"faction": "neutral", "gx": 60, "gy": 36, "rotation": 0, "targetSize": 6.0 }
]
}
Field notes:
elevOffsetshere is illustrative:...,4.0cells (> 3.5 cap) form a small cliff wall;...,1.2cells stay walkable high ground beside it — a readable ridge with a passable shoulder.- The
orehalf-extents (rx/ry: 3) give each base a 7×7 blob at ~mirror distance (fairness gap ≈ 0). The centralderrickis the contested prize. - Real maps ship far more props (§8 density floor) and a full
terrainRowsgrid; this is the minimum skeleton the validator passes with zero errors.
Cross-references: docs/custom-maps.md (multiplayer packaging/approval),
docs/skirmish-map-audit.md (per-biome dressing recipes),
backlog/1-0-director-audit/PER_MAP_DELTA_AUDIT.md (roster density/variety/fairness norms).