scmJS docs

Core concepts

The rules on this page hold across the whole API. The rest of the guide refers back to them instead of repeating them.

Units, players and ids#

Asynchronous calls, and the one synchronous builder#

Asynchronous calls return promises. There are no completion callbacks. When the user dismisses something (Esc, Cancel, a right-click, the ×), the promise resolves with null or false instead of rejecting, so the normal path needs no try. This covers opening, saving and exporting maps, loading game data, and everything that waits for the user (pickers, confirm, prompt and so on).

const rect = await api.ui.pickArea({ prompt: "Pick an area to flatten" });
if (rect) {                                   // null: Esc, a right-click, or no map
  await api.tileset.load();                   // the graphics the fill needs
  const [, ground] = api.terrain.types();     // the tileset's second flat terrain
  if (ground) api.document.edit("Flatten", (tx) => tx.stampTerrain(rect, ground.id));
  else api.ui.status("The tileset graphics are not installed.");
}
Try it

The remaining callbacks are real callbacks: event listeners, widget handlers, a dialog's mount, and the pointer and draw hooks of map tools and overlays.

A transaction's function must be synchronous. document.edit(label, build) and document.update(label, build) commit the moment build returns. An async function would commit at its first await, and the rest would change the map outside the undo entry. TypeScript refuses one, and the editor catches it at run time too.

Wrong, because the edit commits at the await and the placement lands outside it:

api.document.edit("Place", async (tx) => {
  await api.data.load();
  tx.placeUnit(0, 0, 128, 128);
});

Right: await first, then write.

await api.data.load();
api.document.edit("Place", (tx) => { tx.placeUnit(0, 0, 128, 128); });
Try it

For long work, api.ui.progress shows a panel over the map that does not block editing. Check cancelled() in your loop. Its signal is an AbortSignal, so a fetch(url, { signal: job.signal }) stops when the user cancels.

The three kinds of write#

Every change a plugin makes to the map goes through one of three calls, matching the editor's own three ways of writing: a brush stroke, a dialog's OK, and a raw file edit.

Call Covers Undo If your function throws
document.edit(label, build) Terrain and objects: tiles, ISOM, units, sprites, doodads, locations, fog. One history entry. Rolled back.
document.update(label, build) Tables and settings: triggers, briefing, strings, switch names, name and description, players, forces, unit / upgrade / tech settings, sounds, map revision. One history entry, as with a settings dialog's OK. What was written stays, as one entry.
document.sections.* The file's raw bytes, any section. None, and the history is cleared. Nothing is written.

In both transactions each operation applies immediately, so a later one sees what an earlier one did. Calling the transaction after the function has returned throws. The transaction marks the map modified, saves the right file sections and repaints; you do none of that yourself.

The map you read is read-only. document.scenario() is the editor's own live object, typed read-only all the way down: a line that writes to it does not compile, because a direct write would skip undo, never be saved and never repaint. Copy what you want to change ({ ...scn.units[0], x: 64 } is an ordinary record) and hand it to a transaction. A helper of your own that only reads takes ReadonlyScenario, or readonly UnitRecord[] for one of its lists.

What you add is removed for you#

Menu items, hotkeys, commands, services, dialogs, panels, overlays, map tools and listeners are all tracked. Turning the plugin off, reloading it or removing it takes them away whether or not you cleaned up.

When no map is open#

The editor can have no map open, and a plugin starts before the first map does.

The game's files may be missing too: the editor works without them, so anything that needs the tileset, the unit graphics or the game's tables degrades. A terrain operation writes nothing and says so in its result, api.data answers null, and api.graphics returns no picture. Check what you get back, and show a name where a picture is missing.

Several maps, and maps to look at only#

Several maps can be open at once, as tabs. Every call works on the map in front.

const notes = new Map<number, string[]>(); // per open map, by its id

api.events.on("document", (e) => {
  const open = new Set(api.document.list().map((map) => map.id));
  for (const id of notes.keys()) if (!open.has(id)) notes.delete(id);
  if (e.id !== null && !notes.has(e.id)) notes.set(e.id, []);
});
Try it

View-only maps. document.open(bytes, name, { viewOnly: { label: "Replay" } }) opens a map to look at, not to change, such as a replay's map or a preview from a website. It always opens beside the others, and its tab and the status bar show the label. On such a map:

Events#

api.events.on(event, fn) returns a Disposable. Listeners run after the change, in plugin activation order, and cannot block or change it. A listener that rewrites the map (Repair does) causes a new "document" event that every listener sees.

Event When
"document" The map in front changed. reason is "open", "new", "close", "replace" (reparsed after a raw edit) or "switch" (another open map came to the front). Carries the map's id.
"commit" One change was committed: its reason, label, the tile area it touched, and which parts changed. For following changes one by one, as Timelapse does.
"terrain" Any committed map edit, undo or redo.
"units", "doodads", "sprites", "locations" That list changed.
"settings", "triggers" A settings dialog's OK; the trigger or briefing list.
"layer", "selection", "clipboard" The active layer, the selection, the marked area or clip.
"view", "tool" The view moved or its options changed; a map tool or pick started or stopped.
"palette", "options" A palette's pick; an editing option or preference.
"modified", "file" The unsaved-changes flag; the file's name, save options, archive files or recent list.
"commands", "services" A plugin added or removed a command or service.
"dialogs" A dialog opened or closed.
"gameData", "language" The game data or the editor's language changed.

Only "document" and "commit" carry a payload. The others say that something changed, and you read the new state from the API. A plugin that acts on maps as they open listens for "document" with reason "open":

api.events.on("document", (e) => {
  if (e.reason === "open") api.ui.status(`Opened ${e.fileName ?? "a map"}`);
});
Try it

The reference has an example of "commit".

scmJS 0.8.5 · Generated from the repository. The source is under the MIT license, which does not cover what ATTRIBUTION.md lists. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.