Every issue the validator reports carries a stable code. There are 93 of them, and this page is generated from the same registry the validator stamps them from, so a code you see in a report is always on this page and a code on this page is always one something can emit.
Switch on the code, never on the message. Messages are written for people and get reworded; codes do not change.
Where codes come from
The same validator runs in four places, so the same code means the same thing everywhere:
- The Problems panel in The Forge, while you work.
- The validator page and
POST /api/validate, before you publish. - Publishing, which refuses errors outright.
- Loading, every time a match opens a custom map: a direct launch, a replay, a host's map arriving at each guest, a
?mod=merge, and The Forge's Test Map. See the load gate below.
Severity and class are different questions
Severity says whether you are blocked: an error stops a publish, a warning does not. Class says what kind of problem it is, and it is the class the load gate reads. Four classes:
| Class | What it means |
|---|---|
security | the content could harm the machine that loads it. Never a warning, anywhere. |
resource | a size, count or per-tick cost budget. The map may be correct and still refuse to run at a playable frame rate, or to fit in a lobby message. |
integrity | the content is inconsistent, or will silently not do what it says: a dangling reference, an entity that never spawns, a match that cannot be won. |
quality | it works; it is probably not what you meant. Typos, imbalance, cosmetics ignored at load. |
The load gate
A map you publish was checked when you published it. A map arriving from someone else was not, so it is re-checked at the door, every time. The gate never repairs a map and never rewrites a byte: two clients repairing the same map differently is a desync, which is worse than a refusal.
A code blocks a load when its class is security or resource, when the code is not registered at all (the gate fails closed), or when it is one of the 9 integrity codes below whose bytes cannot be read. Everything else loads. Codes that block are marked blocks loading in the tables.
This build's gate is set to warn. In warn it records the verdict to the browser console and lets the match start anyway; only enforce refuses. The mode is a build-wide setting (NEXT_PUBLIC_LOAD_GATE_MODE) and never a per-client one, because a gate that refused on one client and not another would half-start a lobby. So treat a blocking code as "this map will be refused as soon as enforcement is on", not as "this map works".
The codes
Grouped by class, alphabetical inside each group.
security (3 codes)
the content could harm the machine that loads it. Never a warning, anywhere.
| Code | Means | Fix |
|---|---|---|
forbidden-key blocks loading | A reserved key appears in an override | Rename the key. "proto", "constructor" and "prototype" are refused because writing them changes the behaviour of every object in the match. |
invalid-id blocks loading | An override id is not a legal id | Use letters, digits, underscores and hyphens only, up to 64 characters. Spaces and punctuation are refused. |
script-banned-syntax blocks loading | A custom script uses something the sandbox will not run | The message names it and the line. Clocks (Date, performance), async/await, BigInt literals, eval and Function, and import/export are all refused: each of them either makes two players compute different matches or goes around the limits on what a script may spend. |
resource (16 codes)
a size, count or per-tick cost budget. The map may be correct and still refuse to run at a playable frame rate, or to fit in a lobby message.
| Code | Means | Fix |
|---|---|---|
invalid-patch-value blocks loading | An override sets a field to a value of the wrong type or outside its range | The message names the field and what it accepts. The bounds are deliberately loose: if a plausible value is refused, the value is not the problem. |
layer-too-large blocks loading | A terrain layer is longer than the map could need | The RLE layer decodes to more tiles than meta.size allows. Re-export from the editor rather than editing the layer by hand. |
map-large blocks loading | The map is large enough to be slow to share | It will still publish. Consider fewer placed entities or a smaller map if lobby joins feel slow. |
perf-hot-periodic-scan blocks loading | A fast periodic trigger scans units every time it fires | Raise the period to 5 ticks or more, or move the scan behind a condition. A region or group scan walks every unit in the match, and at 20 ticks a second that is the whole map per second. |
perf-periodic-group-loop blocks loading | A periodic trigger loops over a unit group | Loop over the smallest group you can (a region or one seat rather than every unit of a type), and raise the period. The body runs once per unit, every time the trigger fires. |
perf-scan-in-damage-event blocks loading | A damage trigger builds a unit group on every hit | The damage events fire once per hit, and there are thousands of hits in a busy fight. Add a filter to the event so it matches far fewer units, or have a periodic trigger refresh a unit-group variable and read that variable inside the damage trigger. A group scan is charged per unit it walks, so one scan per hit spends the tick's whole expression budget in a fraction of a second and every trigger on the map then reads zeros until the next tick. |
perf-uncapped-periodic-spawn blocks loading | A periodic trigger spawns units with nothing to stop it | Add a condition that caps the population (for example, stop once the group is over N), set the trigger to run once, or have it disable itself. Otherwise the unit count rises for the whole match. |
scripts-too-large blocks loading | The map's compiled scripts are too large | Every player parses and holds all of it before tick 0. Move tables and level data out of the code and into the map, and delete unused code. |
too-large blocks loading | The file is over the size limit | Cut placed entities, trigger text or embedded overrides. A multiplayer lobby sends the whole map to every player. |
too-many-entities blocks loading | The map places too many entities | Remove placed units, buildings, resource nodes or spawner squad entries until the total is under the cap. |
too-many-entity-props blocks loading | The map places too many destructible or selectable props | Each destructible or selectable prop is a simulated entity. Replace some with decorative props, or remove them, until the count is under the cap. |
too-many-issues blocks loading | Too many problems to list | Fix the ones listed and validate again; the rest appear once there is room for them. |
too-many-props blocks loading | The map places too many props | Remove props until the map is under the cap; both placement lists count. Past it they are dropped at load. |
too-many-regions blocks loading | The trigger script defines too many regions | Regions are checked against every moving unit. Merge or delete regions until the script is under the cap. |
too-many-scripts blocks loading | The map carries too many custom scripts | Each script gets its own fixed block of interpreter memory, reserved before the match starts. Merge scripts that belong together until the map is under the cap. |
too-many-spawns blocks loading | The map declares too many spawn points | A match seats at most eight players. Delete the extra spawn points. |
integrity (52 codes)
the content is inconsistent, or will silently not do what it says: a dangling reference, an entity that never spawns, a match that cannot be won.
| Code | Means | Fix |
|---|---|---|
aid-collision | Two placed entities share an authored id | Authored ids are how triggers name a specific entity. Rename one; the trigger that looks the id up otherwise finds whichever it meets first. |
building-terrain-mismatch | A building is placed on terrain it cannot sit on | Move the building, or paint the tiles under its whole footprint to terrain it accepts. |
custom-runtime-invalid | Unsupported custom-game runtime | Use a supported controller and timing profile. Arcade controllers require local simulation. |
def-override | A problem in the data overrides | The path names the override and the field. Fix it in the Data module, or remove the override. |
def-reference | A data override points at something that does not exist | Correct the id it names. An override whose base cannot be resolved is dropped at load. |
duplicate-spawn | Two seats share a spawn point | Move one of them. Two starting bases on one tile overlap and one of the two loses its buildings. |
editor-draft blocks loading | This is an editor draft, not a game map | Use File ▸ Export or Test Map in The Forge, which writes the game format, or validate through /api/validate, which converts drafts for you. |
empty blocks loading | The file is empty | Export the map or fragment again; an empty file usually means a save that failed. |
entity-out-of-bounds | A placed entity is off the map | Move it inside the map. An entity outside the bounds is dropped at load without a message. |
format-too-new blocks loading | The map was made by a newer build | Update the game. This build cannot read the map format the file declares, and reading it anyway would misplace entities. |
format-version-understated blocks loading | The map declares an older format than it uses | Re-save the map from this build so it is stamped with the format version its trigger nodes and its props require; older builds must refuse it rather than silently ignore them. |
height-rule-failed | The height validator failed | This is a bug in the game, not in your map. Report it with the map attached. |
invalid-env | A meta.env field is the wrong type or out of range | Fix the field named in the message. timeOfDay is 0..1, dayNightSpeed and fogDensity are finite and not negative, lightTint is a 24-bit integer, sky needs all three of horizon, zenith and haze. |
invalid-prop-block | A prop's blocking override is malformed | Write block as "none", or as an object with "layers" (layer names) and/or "cells" (whole-number [x, y] pairs near the prop). Unreadable parts are dropped at load, which changes what the prop blocks. |
invalid-tileset-source | A meta.tilesets entry is not a slot source | Each entry is { "biome": "<biome>", "variant": <whole number ≥ 0> }; variant is optional. |
json-syntax blocks loading | The file is not valid JSON | Open the file and look near the reported line for a trailing comma, an unquoted key or a missing bracket. |
lava-rule-failed | The lava validator failed | This is a bug in the game, not in your map. Report it with the map attached. |
layer-malformed-run | A run in a terrain layer cannot be read | A row is comma-separated runs, each "<id>" or "<count>x<id>", with no other separator or punctuation. Decoding stops at the first run it cannot read and the rest of the row loads as grass, silently — so check the separators before anything else. |
layer-row-count | A terrain layer has the wrong number of rows | The layer needs exactly meta.size rows, separated by "|". Rows past the end are dropped unread and rows that are missing load as grass, so re-export the map rather than adding or deleting rows by hand. |
layer-row-width | A row of a terrain layer is the wrong width | Every row must decode to exactly meta.size tiles — add up the run counts. Tiles a row leaves undescribed load as grass, and tiles past the width are dropped unread. |
malformed-meta blocks loading | The map header is missing or wrong | Check that meta.size is a whole number inside the allowed range and that the terrain layer is present. |
malformed-prop | A prop placement is not a placement | Write each prop placement as an object with a "typeId" string and numeric "x" and "y". Anything else is dropped at load. |
map-rule | A problem a map rule reported under a code of its own | Read the message: the rule that found it is not one of the shipped rules, so there is no page for its code. |
merc-camp-in-buildings | A mercenary camp is in the buildings list | Author it under mercCamps. A merc camp placed as a plain building spawns with no hireable stock: it looks right and does nothing at all. |
mod-package | A problem in the mod package | The path names the field. Fix it in the .sosmod, or re-export the package. |
monument-terrain-mismatch | The monument is not somewhere it can be held | Move the monument onto terrain units can reach and stand on. |
multiple-monuments | More than one monument | King of the Hill is decided by one monument. Delete the extras. |
no-gold-mines | The map has no gold mine | Place a gold mine per starting area, or turn standard victory off if this map is not meant to be an economy map. |
no-monument-for-hill-victory | King of the Hill with no monument | Place the monument the mode is won by, or pick a different victory preset. |
no-ramp-off-start-plateau | A starting plateau has no ramp off it | Carve a ramp. Without one, the seat is walled in by its own cliffs and cannot leave. |
overlapping-footprint | Two buildings occupy the same tiles | Move one. Only one of a pair of overlapping buildings survives placement. |
prop-off-tile | A prop that must sit on a whole tile sits between tiles | Use whole-number x and y for anything that blocks movement or is an entity, for the pre-props catalogue props, and for every placement in the pre-props list that older builds read, because all of those are drawn or blocked by tile. Other decoration may sit between tiles; the rest is rounded to the nearest tile. |
prop-out-of-bounds | A prop is off the map, or hangs over its edge | Move it inside the map. A prop whose anchor is off the map is dropped at load; one whose footprint runs past the edge is drawn hanging off it. |
root-not-object blocks loading | The top-level value is not an object | A map, trigger script, override set and mod package are all JSON objects. An array or a bare value cannot be one. |
roster-creep-seat | A roster entry targets the creep seat | The creep seat is not a playable slot. Give the entry a seat inside the playable range. |
roster-rule-failed | The roster validator failed | This is a bug in the game, not in your map. Report it with the map attached. |
script | A problem in one of the map's custom scripts | The path names the script and the field. A script needs a unique id, a lang of "ts" or "js", its source, the compiled output of that source, and the API level it was written against. |
script-compiled-mismatch | A script's stored program is not what its source compiles to | Open the script in The Forge and save it again, which recompiles it. The game runs the stored program while everything anyone reads shows the source, so the two are only allowed to differ by being fixed. |
scripts-not-syntax-checked | A custom script could not be checked | Validate the map with the in-game validator, the /tools/validator page, or npx tsx tools/validate.ts. A script that has not been checked is reported rather than assumed safe. |
single-player-standard-victory | A one-seat map still uses standard victory | Standard victory ends the match the moment there is one player left, which is tick one here. Turn it off in Map ▸ Rules, or add an opposing seat. |
spawn-not-walkable | A spawn point is on terrain nothing can stand on | Move the spawn point onto land, or paint the tile walkable. |
spawn-out-of-bounds | A spawn point is off the map | Move the spawn point inside the map. A seat that starts off the map never gets its starting town hall. |
too-few-spawns | Not enough spawn points for the seats the map declares | Add a spawn point per seat, or declare fewer seats in the Players panel. |
trigger | A problem in the trigger script | The message names the node and the field. Open that trigger in The Forge and fix the field it names. |
unit-terrain-mismatch | A unit is placed on terrain it cannot occupy | Move the unit, or change the tile. A land unit on water is pushed to the nearest legal tile, which may be across the map. |
unknown-allowed-building | The allowed-buildings list names a building nothing defines | Correct the id. An unknown entry silently allows nothing, so the seat loses a build option it was meant to have. |
unknown-building-type | A placed building has a type id nothing defines | Correct the id, or add a defOverrides entry that defines it. An unknown type never spawns. |
unknown-format blocks loading | Cannot tell what this file is | Add a "$schema" pointing at https://shardsofstone.com/schemas/v1/… , or pass an explicit kind (--kind map). |
unknown-prop-type | A placed prop has a type id nothing defines | Correct the id (the message suggests the closest one), or remove the placement. A prop of an unknown type is never drawn. |
unknown-terrain-id | A terrain layer uses a tile id that is not a terrain type | Use one of the tile ids the editor paints. An id outside the terrain list is walkable by nothing, buildable by nobody and drawn with no texture, and nothing reports it at load. |
unknown-unit-type | A placed unit has a type id nothing defines | Correct the id, or add a defOverrides entry that defines it. An unknown type never spawns. |
unparseable blocks loading | The map data could not be read | Re-export the map from The Forge. A map that will not parse was almost always truncated in transit. |
quality (22 codes)
it works; it is probably not what you meant. Typos, imbalance, cosmetics ignored at load.
| Code | Means | Fix |
|---|---|---|
invalid-city-tuning | A city tuning value in rules.city.tuning is unknown or out of range | Use a setting, service, need, tool or people the message lists, as a whole number in its range. The city ignores the bad value and plays with its usual number there. |
invalid-prop-city | A city building's options are malformed or out of range | Use only the city options and values the message lists. Unknown options are ignored, and colours too dark or too bright are toned into range. |
loading-screen-ignored | The loading screen art will not be shown | Point it at an artwork path that exists in the asset manifest, or drop the field. |
no-gold-source | No seat has a way to earn gold | Give the seats starting gold, place a mine, or have a trigger grant gold. Otherwise nothing can ever be built. |
prop-transform-clamped | A prop rotation, scale or variant is outside what the prop allows | Use a value the prop accepts: whole degrees 0 to 359 (90-degree steps for props that snap, 0 for fixed ones), a scale inside its scale range, a variant below its variant count. Other values are clamped when the map is drawn. |
resource-imbalance | One seat starts with far more resources nearby than another | Add or move resource nodes so the starting areas are comparable, or accept it if the asymmetry is the point. |
spawner-empty-squad | A spawner has an empty squad | Give the spawner at least one unit type, or delete it. An empty spawner is a prop. |
trigger-angle-in-degrees | An angle looks like degrees in a slot that wants radians | Divide by 57.29577951308232, or write the radian value. Every trigonometry node in the trigger language takes radians; create units facing is the one field that takes degrees, and mixing them fails silently because the wrong angle is still a perfectly good number. |
trigger-buff-already-expired | A buff is added with a duration that has already run out | add buff takes a duration in TICKS — 20 of them to the second — and -1 for a permanent buff. Zero or any other negative number is a buff that is over before it starts, so the action stores nothing and does nothing. It is silent at runtime because an expired buff is indistinguishable from no buff, which is why this is reported here instead. |
trigger-buff-id-reserved | A buff id uses a prefix the engine owns | Pick an id that does not start with item:, estatus:, aura_, well_fed: or flag:. The engine clears each of those namespaces wholesale — unequipping an item, an elemental status ending, a meal wearing off — so a buff hiding inside one is deleted by something the map never asked about, at a moment nothing in the map explains. |
trigger-hero-level-not-raised | A hero level is set to a level the node can never reach | set hero level only RAISES. Heroes start at level 1 and the engine records the level but not the growth applied to reach it, so there is no honest way to take a level back — which is why lowering is a no-op rather than an approximation. A literal 1 or below therefore does nothing at all. To reset a hero, remove it and create it again. |
trigger-item-count-clamped | An item count is above the per-action cap and will be clamped | One give item hands over at most 64. The count drives a loop of single-item inserts (so the bag's own stacking rules apply) and overflow past a full bag becomes ground items, so an unbounded count would be a hang and an entity flood. Call the action more than once, or ask for a number the bag can hold. |
trigger-multiboard-cell-before-set | A multiboard cell is written to a board the map never creates | Add a create or replace a multiboard for that board id — on map initialisation is usual — before anything writes a cell into it. A cell written to a board that does not exist is dropped, and at runtime that is indistinguishable from a cell nobody ever wrote, which is why this is reported here. |
trigger-multiboard-cell-out-of-bounds | A multiboard cell falls outside the grid its board declared | Rows and columns are 0-BASED, so a board of 3 rows has rows 0, 1 and 2 — writing row 3 is one past the end. Either widen the board in create or replace a multiboard, or move the cell inside the grid it already has. The write is dropped, silently, because the board simply has nowhere to put it. |
trigger-objective-campaign-only | A campaign objective is set in a map that is not a campaign mission | set a campaign objective writes the CAMPAIGN's own mission objectives, which only exist inside a campaign mission — in a skirmish, a multiplayer match or a published custom map it does nothing at all. For a goal list of your own use set quest, which is the trigger layer's quest log and works everywhere. |
trigger-sort-order-discarded | A sorted group is stored in a variable, which re-sorts it | Use the sorted group inline — loop over it, or read a position out of it. Unit-group variables are always re-sorted by unit id on write, which is what keeps every other group identical on every client, so storing a sort throws the sort away. |
trigger-sound-first-play-silent | A sound is played once, and the first play of an unloaded sound is silent | SoundManager fetches and decodes a sound on FIRST request and returns without playing it — so a cue fired exactly once, on the tick it matters, plays nothing at all. Only about a quarter of the shipped library is preloaded. Play the sound somewhere harmless earlier in the map (map initialisation is ideal) so it is resident when the moment comes, or choose a sound the match has already made. |
trigger-timer-window-unknown-timer | A timer window names a timer nothing ever starts | A timer window is a label on a clock, and the clock is the named timer — so a window for a timer no start timer ever starts is a title above a countdown frozen at zero. Start the timer somewhere, or name the timer that is already running. |
unknown-biome | The biome name is not one of the shipped biomes | Use one of the biome names the editor offers; an unknown biome falls back to forest at load. |
unknown-patch-field | An override sets a field no definition of that kind has | Check the spelling against the suggestion in the message. Nothing reads the field, so the override has no effect. |
unknown-tileset-biome | A tileset slot is sourced from a biome that does not exist | Use a real biome name. An unknown source silently falls back to the map's own biome, so the borrowed texture never appears. |
unknown-tileset-slot | A tileset slot name is not a real slot | Use a slot the tileset panel lists (rock_1 … shore_5). An unknown slot is ignored at load, so the map draws its own biome there. |
If your code is not here
Then it did not come from the shared validator. The most likely cause is a newer build reporting a code this page predates, so check the version in the report first. Failing that, an unregistered code is treated as blocking by the load gate, which is the conservative answer rather than a judgement about your map.
Related
- Validation and tooling for how to run the validator.
- Performance and limits for what the
resourcebudgets actually are. - Determinism for authors for why some of these are errors rather than warnings.
- Compatibility and versioning for the two format-version codes.