Plugins

A plugin is a plugin.ts or plugin.js next to a plugin.json manifest file. Plugins can read and edit the open map and add menu items, tools, dialogs and panels. Install them from Plugins ▸ Browse Plugins… in the editor.

Add your own plugin to the list by filling out this issue form.

This list is read live from the same index the editor's Browse Plugins reads (scm-js/registry), so the two always match.

Loading the registry…

Writing one

The API

A plugin is two files: a plugin.json saying where the code is, and an entry file whose default export is activate(api). The editor calls it once when the plugin is switched on and hands over api.

Through it you read the open map and write to it, add to the chrome (menu and context items, hotkeys, dialogs, floating panels, map tools that own the pointer, overlays drawn on the canvas), and read the game's own decoded .dat tables. Anything you register is automatically taken away when the plugin is switched off.

Every write goes through api.document.edit(label, tx => …). Whatever the callback does becomes one undo entry, marks the CHK sections it touched, and repaints. The plugin's edit then undoes and redoes like any other stroke.

The API is promise-first. Anything asynchronous returns a Promise, and activate and a dialog button's run may be async. A dismissal resolves to null or false; it does not reject. The exception is the callback you hand to edit. It commits the moment the callback returns, so an async one would commit only what ran before its first await, and the rest would write outside the entry. Await the graphics, the map pick or the fetch first, then write in one go. TypeScript refuses an async builder, and a plain-JavaScript one is caught at run time.

export default function activate(api) {
  api.menu.add("Tools", {
    label: "Marine here…",
    enabled: () => api.document.isOpen(),
    async run() {
      // Asynchronous, so await it. Esc or a right-click resolves null.
      const at = await api.ui.pickTile({ prompt: "Where does the marine go?" });
      if (!at) return;

      // Synchronous, so the whole thing is one undo entry called "Place a marine".
      api.document.edit("Place a marine", (tx) => {
        const px = api.consts.TILE_PX;
        tx.placeUnit(0, 0, at.x * px + px / 2, at.y * px + px / 2);
      });
    },
  });
}

TypeScript is transpiled in the browser, so a one-file plugin needs no toolchain. A repository that wants npm dependencies commits a bundle and names it in the manifest.

Getting listed

A repository in the scm-js organisation named plugin-…, or carrying the scmjs and plugin topics, is picked up by the registry generator at its newest semver tag. Anything else installs by pasting its address into the editor.