Plugins

A plugin is a plugin.ts or plugin.js next to a plugin.json manifest file. The plugin API is powerful, allowing for extensive customization and additional functionality. Access them in Plugins ▸ Browse Plugins from inside the editor.

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

This list is the same index the editor's own browser reads (scm-js/registry) and is a live reflection of what the editor currently offers.

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 - this results in the plugin's edit hooking into undo/redo like any other stroke.

The API is promise-first: anything asynchronous returns a Promise, activate and a dialog button's run may be async, and a dismissal resolves null or false rather than rejecting. The one exception is the callback you hand to edit: it commits the moment that callback returns, so an async one would commit whatever ran before its first await and let the rest write outside the entry. Await the graphics, the map pick or the fetch first, then write in one go. TypeScript refuses an async builder outright, and a plain-JavaScript one is caught at runtime.

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 at all; 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.