Map Editor Documentation

Validation & Tooling

One validator, four front ends: the Validation module, the web validator, POST /api/validate and the command line. Plus the JSON Schemas, the Trigger Debug panel and a write, validate, fix loop for people and AI agents.

Test & publish · Page 14 of 46

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).

SeverityEffect
ErrorBlocks 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.
WarningBlocks 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/json

No 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).

StatusBodyWhen
200the report aboveValidation 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:

  1. a $schema naming one of the schemas below decides outright;
  2. a map: terrain, or meta together with spawns or v (meta alone is not enough), or a Forge draft (a numeric width and a tiles array, with no meta);
  3. a mod: string id and version plus any of author, formatVersion or defOverrides;
  4. a trigger script: version with a triggers, vars or regions array;
  5. data overrides: a units, buildings, items, spells, upgrades or heroes array;
  6. a mod again: a string name with a defOverrides object.

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:

LimitValueOver it
Any single file4 MBNot parsed at all: too-large, and kind is null
A map4 MBtoo-large; over 1 MB it still validates, with a map-large warning
A trigger script or override set on its own1 MBtoo-large
A .sosmod package4 MBtoo-large
Map size (meta.size)8 to 512 tiles, a whole numbermalformed-meta
Placed entities (buildings, units, mercenary camps, critters, pools, ground items, props, gold mines, oil patches and spawner squad entries, together)20,000too-many-entities
Spawn points64too-many-spawns
Regions in a map's trigger script, or in a trigger script validated on its own2,000too-many-regions
Issues in one report500 errors and 500 warningsThe 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 --json

Text 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:

  1. among the .sosmod files named in the same run;
  2. as <id>.sosmod beside the file that needs it;
  3. as public/mods/<id>.sosmod in 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:

SchemaFor
/schemas/v1/map-format.jsonA playable map
/schemas/v1/trigger-script.jsonA trigger script
/schemas/v1/def-overrides.jsonA set of data overrides
/schemas/v1/mod-package.jsonA .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.

  1. 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.
  2. Write the trigger script, override set or mod as a file.
  3. Validate it: npx tsx tools/validate.ts file.json --json, or POST it to /api/validate.
  4. Fix exactly what is reported. Each issue's path points at the offending field, and suggestion, when present, is usually the fix. Do not rewrite the parts that passed.
  5. Repeat until valid is true, then read the warnings.
  6. Optionally, run it headlessly. With a checkout of the repository, npx tsx tools/ai_loop/playMap.ts map.json --json plays 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.
  7. 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.