DOMINATION

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:

  1. Generator runsGridMap.GenerateFromDescriptor produces a base grid from biome
    • seed + generatorParams. For generator: "none" this is a flat canvas.
  2. Tile overlayApplyTileOverlay: the authored terrainRows grid is stamped over the generated base, then any sparse overlayTiles / terrainLayers.
  3. Resource overlayApplyResourceOverlay: ore blobs stamp walkable ore ground; derrick nodes are recorded for the world-build pass.
  4. Elevation sculpt LASTelevOffsets per-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 w and wadeable shallow h cells in terrainRows/overlayTiles, or a generator's river. Exact, deterministic, no noise. Both share one visible water surface; h exposes a lighter submerged shelf and excludes ships/whitecaps while remaining wadeable by infantry and amphibious units.
  • Elevation-carved water — the River Carve brush writes w tiles and feathers banks via elevOffsets. 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., no terrainRows): 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.5 on flat base ground.
  • Guaranteed walkable iff base_max + dh ≤ 3.5.
  • Rock cliffs come from the +4.5 elevation bump (k tile), not the passable flag.
  • The Elevation tool clamps offsets to ±4; keep tactical high-ground ≤ 3.5; use k for 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 explicit k cliff 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→B is 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 at dh = 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. Its elevOffsets string is long (~14 KB) precisely because it is a dense wall of ...,5 entries.
  • Central walkable hill + rocky crown — high_command (256×184). A smooth walkable dome (offsets ramp 0.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.0 mesas 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 w or wadeable shallow h in terrainRows (Terrain tool). Use h only where ground traversal is intentional. River profiles are opaque, so their submerged gameplay shelf is deliberately hidden; naval units and open-water whitecaps still exclude h. Add m (mud) or s (sand) banks for readable shorelines — the generator's PaintBanks does this automatically on preset maps.
  • River Carve brush (Elevation tool → River Carve): drag to carve a river — the core band becomes water and the banks are feathered to an intermediate height via elevOffsets for 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-extents rx/ry (a 2×2 half-extent = 5×5 blob). ApplyResourceOverlay levels 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 a fuel_depot (6 barrels) or barrels (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_AUDIT target 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, patrol flag) 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 with radius ≤ 0 warn.
  • Triggers (TriggerData): conditions + actions, targetId must resolve to a real entity id (spawn/zone/strategic/placed/waypoint/path/trigger). A trigger with no conditions and no actions warns; a dangling targetId errors.

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). night also switches on warm emissive building-window glow (bloom-lit, no per-building lights). Empty = the noon default (unassigned maps are unchanged). Supersedes timeOfDay when both are set. Curated values live in Assets/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:

  1. Reconstruct the walk grid from the descriptor alone. Start from terrainRows (w/b/k/o semantics per §2), then apply elevOffsets additively. A cell is walkable iff its tile is passable AND final elevation ≤ 3.5 (the cliff rule, §3). Because the authored region is deterministic (no PerlinNoise when terrainRows is a full grid), the grid you reconstruct is exactly what the engine builds.
  2. 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.5 and guaranteed-cliff iff dh > 3.5. Authored terrainRows maps skip this — they're exact.
  3. 4-connected BFS from p1. Assert every other p* start, every ore/derrick, and every strategic point is reached. 100% reached = connected. This mirrors the engine's own MapValidator.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:

  1. Save + Validate in-editor (V) — zero errors; justify every warning.
  2. unity/compile-check.sh — if any C# changed (0 errors).
  3. unity/run-tests.sh — logic invariants (placement + elevation round-trip) after any logic/data change; exits nonzero on failure.
  4. -executeMethod Build.ValidateAllMaps — sweeps all ~48 maps → ranked docs/map-validation-report.md (skirmish first, by severity). Re-run after a batch.
  5. -executeMethod MapQualityAudit.CheckShippedMapQuality — the CI gate (zero shipped errors, no ≥ 90% terrain clone).
  6. Preview bake (§12) if the map is new or its terrain/spawns changed.
  7. Overview captureBuild.CaptureMapOverviewAuthored -mapId <id> -out <path>, or a unity/build-game.sh + numbered unity/shots/NN_description.png for the visual audit trail — verify the shot shows the change.
  8. QA-gate (domination-qa-tester) before merge; branch + commit the instant edits exist (a concurrent git reset --hard wipes 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:

  • elevOffsets here is illustrative: ...,4.0 cells (> 3.5 cap) form a small cliff wall; ...,1.2 cells stay walkable high ground beside it — a readable ridge with a passable shoulder.
  • The ore half-extents (rx/ry: 3) give each base a 7×7 blob at ~mirror distance (fairness gap ≈ 0). The central derrick is the contested prize.
  • Real maps ship far more props (§8 density floor) and a full terrainRows grid; 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).