Everything you author (a map, a trigger script, a set of data overrides, a
.sosmod package) goes through one validator. The editor, the web
validator, the HTTP endpoint, the command-line tool and the server's publish
check all call the same function, so they cannot disagree about whether
something is valid.
The validator does not judge your design. It reports things that would fail silently at runtime: a misspelled field in a trigger, a unit type that does not exist, a region a trigger names but the map never declares, a mercenary camp that would spawn as scenery. Each problem has a severity, a JSON path to the exact spot and, where something is close, a suggestion.
Data override patches are checked too: a field the definition does not have is
an unknown-patch-field warning with the closest real name (see
Data Overrides).
| Severity | Effect |
|---|---|
| Error | Blocks Test Map and publishing. Saving is blocked only by the editor's own terrain problems; rule, trigger and data errors are counted when you save but never stop the save, so half-finished work is not lost. At runtime the offending part degrades rather than crashing, which is exactly why it is caught here. |
| Warning | Blocks nothing. Worth reading, because most warnings are mistakes most of the time. |
In The Forge: the Validation module
Press F10. Every problem in the map is listed, with a jump-to for anything
that has a location. The count in the status bar is the same list. See
Testing for how to read it and
Publishing for the rules that only apply to
publishing.
In the browser: /tools/validator
/tools/validator runs the validator entirely in your
browser. Paste or drop a file (.json, .sosmap, .sosmod), or start from a
sample (Minimal TD, Custom Units, Sample Mod, Template). The mode tabs force
a kind if detection guesses wrong: Auto-Detect, Full Map, Trigger
Script, Data Overrides, Mod Package.
When the content is valid, Test in Game launches it: a map as itself, a trigger script or override set wrapped in a blank template map, a mod on a blank map. Copy Clean JSON and Download File give you back the normalised file.
For a mod with dependencies, a Dependencies panel lists each one. Those
hosted on the site are loaded from /mods/<id>.sosmod, the same place the game
loads them from. + Add .sosmod adds any other package to check against.
Each dependency the validator has is marked as checked. One it does not have is
marked not available, and anything the mod uses from it is a warning rather
than an error (see Mods). Test in Game still loads
dependencies only from /mods/.
Over HTTP: POST /api/validate
POST https://www.shardsofstone.com/api/validate
Content-Type: application/jsonNo account or key is needed. The body is either the artefact itself, or a wrapper:
{ "kind": "triggers", "content": { "version": 1, "vars": [], "regions": [], "triggers": [] } }kind is auto (the default), map, triggers, defOverrides or mod, and
can also be passed as ?kind= in the URL. content may be an object or a JSON
string. The wrapper is recognised only when its keys are content plus,
optionally, kind and dependencies.
dependencies is an array of up to 32 .sosmod packages, each an object or a
JSON string, that a mod is validated against:
{
"kind": "mod",
"content": {
"id": "my_mod", "name": "My Mod", "version": "1.0.0", "author": "me",
"dependencies": [{ "id": "their_mod", "version": "^1.0.0" }],
"defOverrides": { "units": [{ "id": "my_knight", "baseId": "their_knight", "patch": { "hp": 90 } }] }
},
"dependencies": [
{
"id": "their_mod", "name": "Their Mod", "version": "1.2.0", "author": "them",
"defOverrides": { "units": [{ "id": "their_knight", "baseId": "dwarf_ironguard", "patch": {} }] }
}
]
}Whatever the mod uses from a package it is sent is checked against that package. The endpoint does not fetch dependencies itself, so a declared dependency that is not sent is not available: what the mod uses from it comes back as a warning, "provided by dependency 'their_mod' — not checked".
The response
A completed validation is always HTTP 200, whether the content is valid or not: the request succeeded, the content did not.
{
"kind": "triggers",
"label": "Trigger Script (TriggerAST)",
"valid": false,
"errors": [
{
"severity": "error",
"code": "trigger",
"path": "$.triggers[0].actions[0].duration",
"message": "unknown property 'duration' on 'showText' — did you mean 'durationTicks'?",
"suggestion": "durationTicks"
}
],
"warnings": [],
"engineVersion": "0.3.4",
"formatVersion": 2
}That error is real output for a trigger script with a typo in it.
engineVersion is the game version that validated it, and formatVersion is
the newest .sosmod format it reads. Issues may also carry at: { x, y }, a
tile position, when the problem has one.
kind is null when the validator could not tell what the content is: it is
not JSON (a json-syntax error, still HTTP 200), it is empty, its root is not
an object, it is too large to parse at all, or its shape matches no kind and
none was given (unknown-format).
| Status | Body | When |
|---|---|---|
| 200 | the report above | Validation ran, including on content that is not valid JSON |
| 400 | { "error": "bad_kind" | "bad_content" | "bad_dependencies", "message" } | An unknown kind; a wrapper whose content is neither an object nor a string; dependencies that are not an array of packages, or more than 32 of them |
| 413 | { "error": "payload_too_large", "message" } | Request body over 5 MB |
| 429 | { "error": "rate_limited", "message" } | More than 30 requests in a minute from one IP address |
| 500 | { "error": "internal", "message" } | A bug on our side |
The endpoint sends Access-Control-Allow-Origin: *, so a web page on any site
can call it.
How the kind is detected
Without kind, the validator looks at the shape, in this order:
- a
$schemanaming one of the schemas below decides outright; - a map:
terrain, ormetatogether withspawnsorv(metaalone is not enough), or a Forge draft (a numericwidthand atilesarray, with nometa); - a mod: string
idandversionplus any ofauthor,formatVersionordefOverrides; - a trigger script:
versionwith atriggers,varsorregionsarray; - data overrides: a
units,buildings,items,spells,upgradesorheroesarray; - a mod again: a string
namewith adefOverridesobject.
If nothing matches, the error code is unknown-format. Set kind when you
know it.
Limits
The same limits apply everywhere the validator runs, including publishing:
| Limit | Value | Over it |
|---|---|---|
| Any single file | 4 MB | Not parsed at all: too-large, and kind is null |
| A map | 4 MB | too-large; over 1 MB it still validates, with a map-large warning |
| A trigger script or override set on its own | 1 MB | too-large |
A .sosmod package | 4 MB | too-large |
Map size (meta.size) | 8 to 512 tiles, a whole number | malformed-meta |
| Placed entities (buildings, units, mercenary camps, critters, pools, ground items, props, gold mines, oil patches and spawner squad entries, together) | 20,000 | too-many-entities |
| Spawn points | 64 | too-many-spawns |
| Regions in a map's trigger script, or in a trigger script validated on its own | 2,000 | too-many-regions |
| Issues in one report | 500 errors and 500 warnings | The rest are counted in one final too-many-issues issue: an error if any errors were left out, otherwise a warning |
The size limits count characters of the JSON text. Over HTTP the request body is capped separately, at 5 MB.
On the command line: tools/validate.ts
In a checkout of the game's repository:
npx tsx tools/validate.ts my_map.json
npx tsx tools/validate.ts public/mods/*.sosmod
npx tsx tools/validate.ts wave_triggers.json --kind triggers --jsonText output is one PASS or FAIL line per file followed by its issues:
FAIL wave_triggers.json [Trigger Script (TriggerAST)] 3 error(s), 0 warning(s)
ERROR trigger $.triggers[0].events[0].region: unknown region 'gaet' — did you mean 'gate'? — gate
ERROR trigger $.triggers[0].actions[0].duration: unknown property 'duration' on 'showText' — did you mean 'durationTicks'? — durationTicks
ERROR trigger $.triggers[0].actions[1].unitTypeId: unknown unit type 'goblin_brawlr' — did you mean 'goblin_brawler'? — goblin_brawler--json prints { "valid", "files": [{ "file", "kind", "label", "valid", "errors", "warnings" }] }.
Exit codes: 0 when no file has an error (warnings are fine), 1 when any
file has an error, 2 for bad usage or a file it could not read. That makes it
usable as a CI check.
A mod is validated against its dependencies, including their own dependencies. Each one is looked for in this order:
- among the
.sosmodfiles named in the same run; - as
<id>.sosmodbeside the file that needs it; - as
public/mods/<id>.sosmodin the checkout, where the game's own hosted mods live.
For a dependency it finds, the tool checks that the version matches and checks everything the mod uses from it against its content. A dependency found nowhere gets a warning that it was not found, and each thing the mod uses from it gets a warning, "provided by dependency 'x' — not checked". Neither changes the exit code. A name that no dependency could provide is an error, as in any other file.
JSON Schemas
The formats are published as JSON Schema, for editor autocompletion and for tools that want a structural check without running the game's validator:
| Schema | For |
|---|---|
/schemas/v1/map-format.json | A playable map |
/schemas/v1/trigger-script.json | A trigger script |
/schemas/v1/def-overrides.json | A set of data overrides |
/schemas/v1/mod-package.json | A .sosmod package |
Put "$schema": "https://www.shardsofstone.com/schemas/v1/mod-package.json" at
the top of a file and most code editors will complete field names as you type.
They also flag unknown fields in trigger nodes, override entries and a
package's top level. They do not flag an unknown field inside a data override's
patch, because the schema accepts any field name there; the validator does,
with an unknown-patch-field warning.
A schema is a structural check. It cannot know that a region id exists, that a unit type is real, or that a variable was declared, so passing the schema is not the same as passing the validator. Use the schema while typing and the validator before shipping.
The node-by-node documentation for triggers, generated from the same catalogue as the trigger schema, is the Trigger Reference.
While a map runs: Trigger Debug
The validator checks shape. Whether your triggers do what you meant is a
runtime question, and the Trigger Debug panel answers it. It exists only in
a Test Map match (Ctrl+F9 from the editor); open it with Ctrl+Shift+F9 or
its button at the lower left.
- Trace lists triggers that fired, conditions that came out false, actions that ran, waits, cap hits (a loop or cascade that hit its limit), cutscenes starting and ending, and every read of something that was not there. Filter by trigger name or id, or by entry kind. Clear empties it.
- Variables shows every global variable's current value.
The trace is recorded only in Test Map and never affects the match.
The write, validate, fix loop
This works the same for a person editing JSON and for an AI agent with a shell.
- Start from real context. In The Forge, File → Copy Map as AI Context copies the map's regions, placed entities, current triggers and overrides, and a full reference of every trigger node, override kind and unit id, all generated from the game's own tables. Or start from the JSON Schemas and the Trigger Reference.
- Write the trigger script, override set or mod as a file.
- Validate it:
npx tsx tools/validate.ts file.json --json, orPOSTit to/api/validate. - Fix exactly what is reported. Each issue's
pathpoints at the offending field, andsuggestion, when present, is usually the fix. Do not rewrite the parts that passed. - Repeat until
validistrue, then read the warnings. - Optionally, run it headlessly. With a checkout of the repository,
npx tsx tools/ai_loop/playMap.ts map.json --jsonplays the whole map in the real simulation, with human seats idle, and exits non-zero if a trigger throws, hits a cap or reads a broken reference. See Web APIs. - Import and play. File → Import from AI / JSON… in The Forge, then
Test Map (
Ctrl+F9) with Trigger Debug open. Validation proves the script is well formed; only playing it proves it is right.
For an agent, the useful stopping rule is "exit code 0 and no new warnings", not "the output looks plausible". The validator rejects the most common invented names outright, so a loop that feeds errors back usually converges in two or three rounds.
Next: Publishing.