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.