For developers
Writing a plugin#
scmjs plugin new "Base Check" writes a plugin to start from in plugin-base-check/: its
manifest, a plugin.ts that adds a Tools menu item, the Korean catalogue and the test
that keeps it complete, the typings, and the build and CI every scm-js plugin uses. Then:
cd plugin-base-check
npm install
npm start
npm start runs scmjs plugin dev. It builds the plugin, serves its folder on your own
computer and opens the editor with it. The first time, the editor asks you to add the
plugin, as for any plugin. After that, each time you save a file the plugin is built again
and the editor reloads it. A build that fails is shown in the terminal and in the editor,
and nothing is reloaded until it is fixed. Ctrl+C stops it.
The editor remembers the plugin by its address, http://127.0.0.1:5310/, so it is the
same plugin next time. --port serves it on another port, and --no-open prints the
editor's address instead of opening it.
scmjs plugin check reads the folder the way the editor loads it and says what would go
wrong: a manifest the editor would refuse, an API version it does not have, a missing
icon, entry or build, a build older than its source, and, for a plugin without a build,
imports of files that are not there or of npm packages. It exits with 1 when the editor
would not load the plugin, so it can stop a CI step. The plugin guide covers
the rest, and map run runs a plugin over a map from the
terminal.
AI tools#
scmjs mcp speaks the Model Context Protocol on
standard input and output, so an AI tool on your computer, such as Claude Code, Cursor or
VS Code, can work on your maps with the same code these commands use. The tool starts
scmjs mcp in your maps folder and talks to it over the pipe; the paths it gives are
files in that folder. Nothing goes on the network except what the commands themselves
fetch: the game engine the first time a map is run in the game, and npm's newest version
for doctor.
With Claude Code, once, in the folder your maps are in:
claude mcp add scmjs -- npx -y @scm-js/cli mcp
Other tools take the same thing as JSON in their MCP settings:
{ "mcpServers": { "scmjs": { "command": "npx", "args": ["-y", "@scm-js/cli", "mcp"] } } }
Each tool is a command from this guide. It takes the same arguments by name and answers
with the command's text and its --json data:
| Tools | The commands |
|---|---|
map_info, map_check, map_dump, map_find, map_diff |
Reading, checking, searching and comparing maps. map_check takes a baseline file and lists only the findings it does not hold |
map_triggers_export, map_triggers_import, map_strings_export, map_strings_import, map_strings_prune |
The triggers and strings as text, in and out. The text is passed inline, so the model edits triggers in the trigger window's text form |
map_set, map_resize, map_new, map_build, map_run |
The Scenario menu's edits, a new map, the TrigScript build, a plugin run over the map |
map_optimize |
The map made smaller; a dry run unless the model asks to write |
map_preview |
The answer is the picture itself, so a model that can see images sees the map |
map_trace, map_cost, map_coverage |
The triggers run in the game, what they cost, and which fired across many replays |
map_balance |
Games played on a melee map with your bots, to see whether its starts are fair; it takes minutes |
replay_info, replay_map |
What a replay holds, and its map written out |
doctor |
What this computer has, so the model knows what it can call |
A map reads as the editor reads it, so what the model is told is what the editor would
show. Tools that change a map write over it unless given out, like the commands, so keep
your maps in git or give the tool copies. map_run runs a plugin, which is code, with
your privileges, as scmjs map run does; the AI tool asks you before it calls a tool, and
you can leave that one unapproved.
map_preview, map_trace, map_cost and map_coverage need your copy of the
game, and the runs fetch the engine the first time; a
trace or cost run takes a few seconds and plays at most ten minutes of game.
map_coverage plays whole replays, so it takes as long as the command does. doctor
tells the model whether the game and the engine are there and what you would need to
install, so it can say so rather than fail. When the game is not in one of the usual
places, the model can pass its folder as starcraft, or you can set SCMJS_STARCRAFT in
the tool's MCP settings.
TrigScript in your text editor#
scmjs lsp is a language server for TrigScript. It speaks the Language
Server Protocol on standard input and output, so VS Code, Neovim, Helix or any editor with
an LSP client can check a script as you type, outside scmJS's TrigScript window. It gives
what that window gives:
- The errors
scmjs map buildwould stop on, TypeScript's and TrigScript's own, checked against the map's names, as you type and when you save. - Completions: the script's own names, the standard library's, a library's exports,
keywords, and after
locations.,units.,switches.orplayers.what the map has. - A hover with the signature and doc comment, what a program's variable holds, and which
of the map's things a name is (
locations.Nestis location 2: 10 × 10 tiles at 48, 6). - Go to definition, into the script's other files, a library's module and the standard
library's (
trigscript/fog). - The outline of a file.
A script is the trigscript/ folder of a map unpacked with
scmjs map unpack, where its files are kept at their paths in
the archive. The map's locations, switches, unit names and sounds are read from
staredit/scenario.chk beside that folder, and read again when the file changes. The
copies of other plugins' libraries that the map keeps are in trigscript/@libraries/. The
round trip is:
scmjs map unpack ridge.scx # ridge/trigscript/main.ts, ridge/staredit/scenario.chk, …
# edit ridge/trigscript/*.ts in your editor
scmjs map pack ridge # back into ridge.scx
scmjs map build ridge.scx --test # the triggers applied and the programs built, as Save does
Any other folder called trigscript is a script too, checked against the names every map
has. For a script kept somewhere else, --map names the map whose names it uses, as a map
file or an unpacked map's folder. With a map file, a .ts file belongs to the nearest
folder above it that has a main.ts. Files that are not part of a script get nothing from
the server, so the server can be on for every TypeScript file.
The server needs TypeScript, as map build does, either in the maps folder's project or
installed globally. Install the command once rather than running it through npx, so the
editor starts it quickly:
npm install -g @scm-js/cli typescript
VS Code. TrigScript's repository has a small extension in
editors/vscode/
that starts the server for the files under a trigscript/ folder. Its README says how to
install it. VS Code's own TypeScript checks the same files without TrigScript's names and
marks program, units and the rest as unknown, so turn that check off for the maps
folder in .vscode/settings.json:
{ "typescript.validate.enable": false }
Neovim (0.11 or later), in init.lua:
vim.lsp.config("scmjs", {
cmd = { "scmjs", "lsp" },
filetypes = { "typescript", "typescriptreact" },
root_markers = { "archive.json" },
})
vim.lsp.enable("scmjs")
On an older Neovim, vim.lsp.start does the same from an autocommand:
vim.api.nvim_create_autocmd("FileType", {
pattern = { "typescript", "typescriptreact" },
callback = function(args)
local root = vim.fs.root(args.buf, "archive.json")
if root then vim.lsp.start({ name = "scmjs", cmd = { "scmjs", "lsp" }, root_dir = root }) end
end,
})
If ts_ls also runs on these files, it reports TrigScript's names as unknown. Leave the
maps folder out of its root_dir.
Helix, in a .helix/languages.toml in the maps folder, so it applies there only:
[language-server.scmjs]
command = "scmjs"
args = ["lsp"]
[[language]]
name = "typescript"
language-servers = ["scmjs"]
Other editors take the same three things: the command scmjs lsp, standard input and
output, and the TypeScript file types. Zed starts language servers only from its
extensions, and there is no TrigScript extension for it yet.
What the server says about each check goes to standard error, which an editor shows as the server's log. It names the script and the map it found, and how long each check took, about half a second for a script that imports the standard library.
In scripts and pipes#
--json prints what a command found as JSON: one object for one file, a list for several.
The text output may change between versions, so scripts should read the JSON.
- as a map's path is standard input, and --out - is standard output. A map read from
- is written to standard output when there is no --out. So the commands chain in a
pipe:
scmjs map new - --size 64x64 --players 2 | scmjs map set - --name "Ridge" | scmjs map check -
cat ridge.scx | scmjs map resize - --size 128x96 --out - > ridge-wide.scx
While a map is on standard output, what the command says goes to standard error, and
--json is refused, since both want the pipe. A map from a pipe has no name: it is
stdin.scx or stdin.chk by its contents, and is written back in the same form.
Colour is off when the output is not a terminal, when NO_COLOR is set, and with
--no-color.
For what the commands do not do, such as a script that edits maps or a check of your own,
the same functions are the @scm-js/core library; the command is built on it.
Exit codes#
| Code | Means |
|---|---|
| 0 | All went well |
| 1 | A map failed its check, two maps differed, a merge had conflicts it was not told how to settle, nothing matched a search, a build did not go through, a map test or a bot failed, a trigger never fired in map coverage --strict, a replay could not be played, or something went wrong |
| 2 | A file was missing or the wrong kind for the command, an option was not valid, you need to sign in first, or the game could not start |