API Reference
The current API version is 1.0.0. Mods declare it via manifest.apiVersion.
The TypeScript source of truth is mod-api.d.ts.
This document mirrors that file.
ctx (Mod Context)
The ctx argument passed to activate(ctx). Internally typed as ModContext.
All mod capabilities are reached through it.
| Field | Type | Purpose |
|---|---|---|
apiVersion | string | Echoes the editor’s API version. |
manifest | Readonly<Manifest> | This mod’s manifest. |
editor | EditorCtx | Active map / layer / tool state. |
map | MapCtx | Read and write tiles, query layers. |
tileset | TilesetCtx | Tileset images and tile properties. |
shadow | ShadowCtx | Shadow layer queries. |
fog | FogCtx | Fog layer queries and config (above-tile graphic layer group). |
panorama | FogCtx | Panorama layers — same surface as fog, beneath the tiles. |
layerGroups | LayerGroupsCtx | Custom graphic layer groups with mod-defined priorities. |
events | EventsCtx | RMXP-style events on a map. |
tools | ToolsCtx | Register custom editing tools. |
menu | MenuCtx | Add menu items. |
commands | CommandsCtx | Cross-mod command registry. |
ui | UiCtx | Panels, dialogs, toasts. |
bus | BusCtx | Subscribe to / emit events. |
fs | FsCtx | Path-scoped filesystem. |
storage | StorageCtx | Per-mod persistent K/V. |
clipboard | ClipboardCtx | OS text clipboard read/write (not the tile clipboard). |
stats | StatsCtx | Read editor usage statistics. |
keybinds | KeybindsCtx | Query and modify keyboard shortcuts. |
i18n | I18nCtx | Translate the mod’s own strings, register whole app locales. |
selectors | SelectorsCtx | Modal pickers (actor, item, switch, map, tileset, audio, graphic, …). |
projectData | ProjectDataCtx | Read-only RPG record lists (actors, items, switches, …). |
mods | ModsCtx | Query other installed mods (presence, status). |
plugins | PluginsCtx | Query installed Essentials plugins. |
log | LogCtx | Namespaced logger. |
lifecycle | LifecycleCtx | Activation hooks. |
editor
editor.version(): stringeditor.activeMapId(): number | nulleditor.activeLayerIndex(): numbereditor.activeTool(): stringeditor.setTool(toolId: string): voideditor.setActiveLayer(index: number): voideditor.listOpenMaps(): number[]editor.gameRoot(): string | nulleditor.brushSize(): numbereditor.setBrushSize(size: number): voideditor.brushTileProperties(): { rotation, flipH, flipV, opacity, hue, saturation, lighting }editor.setBrushTileProperties(props: Partial<{...}>): voideditor.hoverTile(): { x, y } | nulleditor.viewport(): { x, y, zoom }editor.setViewport(viewport: { x?, y?, zoom? }): voideditor.viewOptions(): { showGrid, showCollision, showEvents, showDim, darkMode }editor.setViewOptions(opts: Partial<{...}>): voidRead/write access to editor state. version() returns the running editor’s
version (e.g. "1.0.0") — the same value the in-app updater compares against;
handy for gating features behind a minStudioVersion. setTool accepts both
built-in ids ("brush", "eraser", …) and ids of mod-registered tools.
setBrushSize changes the active brush size. setBrushTileProperties merges
partial props into the current brush. setViewport pans/zooms the active map.
editor.theme(): "dark" | "light"editor.animationFrame(): numbereditor.requestRedraw(): voideditor.setStatusBarText(text: string | null): voideditor.recentMaps(): number[]editor.projectName(): string | nulltheme() is a shorthand for viewOptions().darkMode ? "dark" : "light".
animationFrame() returns a coarse counter (~10fps) useful for overlay animation.
requestRedraw() fires a mod:requestRedraw DOM event for canvas re-renders.
setStatusBarText(text) sets a transient mod-owned status message (pass null to clear). Auto-cleared on mod unload.
recentMaps() returns open map IDs most-recent first. projectName() returns the game folder name.
map
map.info(mapId): PublicMapInfo | nullmap.layers(mapId): PublicLayerInfo[]map.readTile(mapId, layer, x, y): numbermap.writeTile(mapId, layer, x, y, tileId, label?): voidmap.batchWrite(mapId, layer, tiles: Map<"x,y", tileId>, label): voidmap.selection(mapId): { bounds, count }map.readTileData(mapId, layer, x, y): PublicTileData | nullmap.writeTileData(mapId, layer, x, y, data, label?): voidmap.selectionTiles(mapId): Array<{ x, y, tileId, layerIndex }> | nullmap.setSelection(mapId, tiles: Array<{ x, y }> | null): voidmap.addLayer(mapId, name?): numbermap.removeLayer(mapId, layerIndex): voidmap.renameLayer(mapId, layerIndex, name): voidmap.setLayerVisible(mapId, layerIndex, visible): voidmap.setLayerOpacity(mapId, layerIndex, opacity): voidmap.createMap(opts: { width?, height?, tilesetId?, name?, parentId? }): Promise<number>map.deleteMap(mapId): Promise<void>map.resize(mapId, newWidth, newHeight, shiftX?, shiftY?): voidmap.renameMap(mapId, name): voidmap.reparentMap(mapId, parentId): Promise<void>map.beginUndoGroup(label): voidmap.endUndoGroup(): voidwriteTile and batchWrite go through the editor’s normal undo/redo
pipeline — your changes are undoable like any built-in edit.
writeTileData writes full per-tile properties (rotation, flip, opacity, etc.)
on extended layers. setSelection creates or clears a tile selection.
Layer management operates on extended layers (3+). createMap creates a new
map file and registers it in MapInfos. deleteMap removes the map file.
map.transformSelection(mapId, opts: { rotation?: 0|90|180|270; flipH?: boolean; flipV?: boolean; incremental?: boolean; colorOverride?: { hue?, saturation?, lighting?, opacity? };}): voidmap.recalculateAutotiles(mapId, layerIndex, tiles: Array<{x,y}>): voidmap.getNativeTileProperties(mapId, layer, x, y): NativeTileProps | nullmap.setNativeTileProperties(mapId, layer, x, y, props: Partial<NativeTileProps>): voidmap.getClipboard(): PublicClipboardData | nullmap.setClipboard(data: PublicClipboardData | null): voidmap.createUndoScope(mapId, label): UndoScopetransformSelection rotates/flips/recolors the current selection, permuting tile positions so the group visually rotates.
recalculateAutotiles fixes autotile seams after bulk writes — call it with the affected tiles.
getNativeTileProperties / setNativeTileProperties read and write per-tile properties on native layers (0-2).
getClipboard / setClipboard read and replace the tile clipboard; setClipboard(null) clears it.
createUndoScope returns a scope object — call .write()/.writeData() to queue writes, then .commit() to apply them as a single undo entry (or .abort() to discard).
tileset
tileset.getImageBlobUrl(tilesetId): Promise<string | null>tileset.getTileProperties(tilesetId, tileId): { passage, priority, terrainTag } | nulltileset.list(): Array<{ id, name, tilesetName }>tileset.info(tilesetId): { id, name, tilesetName, autotileNames, panoramaName, fogName, battlebackName } | nulltileset.setTileProperties(tilesetId, tileId, { passage?, priority?, terrainTag? }): Promise<void>tileset.currentId(): number | nulltileset.current(): { id, name, tilesetName, autotileNames, panoramaName, fogName, battlebackName } | nulltileset.mapTilesetId(mapId): number | nullThe blob URL stays valid for the lifetime of the editor session.
list returns all loaded tilesets. info returns tileset metadata including
the autotile name array, panorama, fog, and battleback names.
currentId returns the tileset currently selected in the tile palette
(not the map’s assigned tileset). Returns null if no map is open.
current returns full info of the currently selected tileset. Equivalent
to tileset.info(tileset.currentId()).
mapTilesetId returns the tileset ID assigned to a specific map.
Returns null if the map is not loaded.
tileset.transformTileProperties(tilesetId, tileId, { rotation?, flipH?, flipV? }): { passage, priority, terrainTag } | nulltileset.resolveTileProperties(tileId, tilesetId?): { passage, priority, terrainTag } | nulltransformTileProperties returns passage/priority/terrain adjusted for the given rotation/flip — passage direction bits are rotated to match the visual transform.
resolveTileProperties resolves tile properties from any tileset (falls back to the active palette tileset when tilesetId is omitted).
tileset.registerTerrainTag({ id, name }): Disposabletileset.registerPriority({ id, name }): DisposableregisterTerrainTag / registerPriority add a named entry to the Tileset
Editor’s Terrain Tag / Priority dropdowns. id is the integer written
verbatim to @terrain_tags / @priorities (Table1, i16, no clamp) when the user
paints that value. Built-in ids are 0–17 for terrain tags (the Pokemon
Essentials defaults) and 0–5 for priorities (0 = ground, 1–5 = tiles
overhead), so use 18+ / 6+ for custom entries to avoid clobbering them.
Duplicate ids are ignored (first registration wins). Both return a Disposable
and are auto-removed when the mod unloads.
The editor only contributes the picker label + selectable range — there is no
runtime dispatcher. To give a tag in-game behavior, read it back in your game
scripts ($game_map.terrain_tag(x, y) — a plain Integer in RMXP/BES/LBDS; resolved
through PBTerrain / GameData::TerrainTag in Pokemon Essentials) and branch on
the value. See the custom-tile-properties example mod.
selectors
Modal pickers that mount the editor’s stock selector dialogs. Every method
returns a Promise that resolves to the picked value or null when the
user cancels.
// RPG records (1-indexed; index 0 reserved for "nil")selectors.pickActor(opts?): Promise<EntityPickResult | null>selectors.pickClass(opts?): Promise<EntityPickResult | null>selectors.pickSkill(opts?): Promise<EntityPickResult | null>selectors.pickItem(opts?): Promise<EntityPickResult | null>selectors.pickWeapon(opts?): Promise<EntityPickResult | null>selectors.pickArmor(opts?): Promise<EntityPickResult | null>selectors.pickEnemy(opts?): Promise<EntityPickResult | null>selectors.pickTroop(opts?): Promise<EntityPickResult | null>selectors.pickState(opts?): Promise<EntityPickResult | null>selectors.pickAnimation(opts?): Promise<EntityPickResult | null>selectors.pickCommonEvent(opts?): Promise<EntityPickResult | null>selectors.pickEntity(kind, opts?): Promise<EntityPickResult | null>
// Systemselectors.pickSwitch(opts?): Promise<EntityPickResult | null>selectors.pickVariable(opts?): Promise<EntityPickResult | null>
// Map / event / tilesetselectors.pickMap(opts?: { value?, title?, includeCurrentMap? }): Promise<EntityPickResult | null>selectors.pickEvent(mapId, opts?: { value?, title?, includePlayer?, includeThisEvent? }): Promise<EntityPickResult | null>selectors.pickTileset(opts?): Promise<PublicTilesetInfo | null>
// File-backedselectors.pickAudio("BGM"|"BGS"|"ME"|"SE", opts?: { initial?, title? }): Promise<AudioPickResult | null>selectors.pickGraphic(subfolder, opts?: { initial?, fields?: GraphicField[], extraFields?: GraphicPickerField[], showGrid?, showHue?, title? }): Promise<GraphicPickResult | null>
// Inputselectors.pickKeyboardButton(opts?: { value? }): Promise<KeyboardButtonPickResult | null>EntityPickResult = { id, name }. AudioPickResult = { name, volume, pitch }.
GraphicPickResult = { name, hue, opacity?, blend?, direction?, pattern?, sheetCols?, sheetRows?, extra? } — the optional props are present only when their fields entry was enabled (custom extraFields values come back under extra, keyed by field key). KeyboardButtonPickResult = { code, label }.
For RPG-record pickers, opts.extras: SelectorExtra[] lets you place synthetic rows
above the real list (e.g. { id: 0, label: "Entire Party" } for actor target params).
For pickEvent, includePlayer adds id -1 and includeThisEvent adds id 0.
// Example — let the user pick an item, then award itconst item = await ctx.selectors.pickItem({ title: "Reward" });if (item) ctx.ui.showToast({ message: `You got ${item.name}!`, level: "info" });
// Example — pick a graphic from Graphics/Pictures with hue sliderconst pic = await ctx.selectors.pickGraphic("Pictures", { showHue: true });if (pic) console.log(pic.name, pic.hue);
// Example — pick a character sheet with the full property set + a custom field.// `fields` enables built-in controls; `extraFields` adds your own (returned in `.extra`).const ch = await ctx.selectors.pickGraphic("Characters", { showGrid: true, fields: ["hue", "opacity", "blend", "direction", "pattern", "sheetCols", "sheetRows"], extraFields: [{ key: "zHeight", label: "Z Height", control: "number", min: 0, default: 0 }],});if (ch) console.log(ch.name, ch.direction, ch.pattern, ch.sheetCols, ch.extra?.zHeight);
// Example — pick BGM with custom initial volumeconst bgm = await ctx.selectors.pickAudio("BGM", { initial: { name: "001-Battle01", volume: 80, pitch: 100 },});Selectors mount the same React components used by the built-in event editor
(EntitySelector, EventSelector, MapSelector, TilesetSelector,
SwitchPicker, KeyboardButtonSelector, AudioSelector, GraphicSelector).
Only one selector can be active at a time — opening a second one before the
first resolves will replace it (the first resolves with null).
projectData
Read-only views of the project-wide RPG record lists loaded from .rxdata.
Use these when you want to enumerate or look up records without showing a
modal picker. Lists are 1-indexed: index 0 is the reserved nil slot
({ id: 0, name: "" }), and IDs map directly to list indices.
projectData.actors(): readonly PublicRpgRecord[]projectData.classes(): readonly PublicRpgRecord[]projectData.skills(): readonly PublicRpgRecord[]projectData.items(): readonly PublicRpgRecord[]projectData.weapons(): readonly PublicRpgRecord[]projectData.armors(): readonly PublicRpgRecord[]projectData.enemies(): readonly PublicRpgRecord[]projectData.troops(): readonly PublicRpgRecord[]projectData.states(): readonly PublicRpgRecord[]projectData.animations(): readonly PublicRpgRecord[]projectData.commonEvents(): readonly PublicRpgRecord[]projectData.records(kind: PublicRecordKind): readonly PublicRpgRecord[]projectData.getRecord(kind, id): PublicRpgRecord | null
projectData.switchNames(): readonly string[]projectData.variableNames(): readonly string[]projectData.maps(): ReadonlyArray<{ id, name, parentId, order }>PublicRpgRecord = { id, name, iconName? }. PublicRecordKind = "actor" | "class" | "skill" | "item" | "weapon" | "armor" | "enemy" | "troop" | "state" | "animation" | "common_event".
// Example — find all common events whose name starts with "boss_"const boss = ctx.projectData.commonEvents().filter((c) => c.name.startsWith("boss_"));mods
Query the other mods the editor currently knows about — for soft/optional
dependencies, feature detection, or integrating with a companion mod. (Hard
dependencies that should block your mod from loading belong in
manifest.requires instead.)
mods.list(): InstalledModInfo[] // every known mod, incl. this onemods.get(id: string): InstalledModInfo | nullmods.isInstalled(id: string): boolean // present in any statusmods.isActive(id: string): boolean // present AND loaded/runninginterface InstalledModInfo { id: string; name: string; version: string; enabled: boolean; // false if user-disabled status: "active" | "error" | "disabled"; // blocked mods are not listed source: "project" | "global";}The snapshot is read-only and reflects the current load cycle. Mods blocked
before load (missing dependency, plugin version block, duplicate id) are not
listed — only mods that loaded (active), failed to activate (error), or were
switched off by the user (disabled).
// Example — only register an integration when a companion mod is active.if (ctx.mods.isActive("com.author.coremod")) { ctx.commands.execute("com.author.coremod:registerExtension", myExtension);}plugins
Query the Essentials plugins installed under <gameRoot>/Plugins/ (Ruby). Same
data the editor uses to validate requires plugin entries, exposed for runtime
feature detection.
plugins.available(): boolean // false on v16/BES (no Plugins/ dir)plugins.list(): InstalledPluginInfo[] // empty when not availableplugins.get(name: string): InstalledPluginInfo | nullplugins.isInstalled(name: string): booleaninterface InstalledPluginInfo { name: string; // meta.txt Name version: string | null; // Version, or null if none declared essentials: string[]; // Essentials — compatible versions, e.g. ["19.1","20"] link: string | null; // Link — homepage / download URL credits: string[]; // Credits — authors requires: PluginRequirement[]; // Requires — needed plugins (any/min version) exact: PluginRequirement[]; // Exact — needed plugins at an exact version optional: PluginRequirement[]; // Optional — load before this if installed conflicts: string[]; // Conflicts — incompatible plugin names}
interface PluginRequirement { name: string; version: string | null; // min version (requires) / exact version (exact), or null}Every field of the plugin’s meta.txt is exposed. Repeatable fields
(Requires, Exact, Optional, Conflicts) collect every line; comma-list
fields (Essentials, Credits) are split and trimmed. Requires / Exact /
Optional values parse as Name or Name,version.
available() is false on projects with no Plugins/ directory (RMXP v16 /
Base Essentials v5), where plugin presence can’t be verified — in that case the
other queries always report “not installed”. Names match the plugin’s meta.txt
Name field (the same string you’d put in a requires plugin entry).
// Example — adapt behaviour to an installed plugin's version.const fp = ctx.plugins.get("Following Pokemon EX");if (fp && fp.version && fp.version >= "1.5.0") { // use the newer plugin feature}Manifest: requires
Mods declare everything they depend on — other installed mods and Essentials
plugins (Ruby) — through one unified requires array in manifest.json. Each
entry is a discriminated union on type:
{ "requires": [ { "type": "mod", "id": "com.author.coremod" }, { "type": "mod", "id": "com.author.utils", "version": "^1.2.0" },
{ "type": "plugin", "name": "My Plugin" }, { "type": "plugin", "name": "Other Plugin", "url": "https://example.com/other" }, { "type": "plugin", "name": "Strict Plugin", "enforcement": "pluginAndVersion", "version": "1.2.0" }, { "type": "plugin", "name": "Optional Plugin", "enforcement": "none", "url": "https://example.com/opt" } ]}type ModRequirement = ModDependency | PluginDependency;
/** A dependency on another installed mod. */interface ModDependency { type: "mod"; /** Reverse-DNS id of the required mod (its manifest `id`). */ id: string; /** * Optional semver range. Recorded for tooling; the loader enforces presence * (the mod is installed and load-ordered first), not the range, in v1. */ version?: string;}
/** A dependency on an Essentials plugin (installed under `<gameRoot>/Plugins/`). */interface PluginDependency { type: "plugin"; /** Plugin name as it appears in meta.txt's Name field. */ name: string; /** URL where the plugin can be found/downloaded (shown in warnings). */ url?: string; /** * How strictly to enforce this dependency. * - "plugin" (default) — block if the plugin is missing; ignore version. * - "pluginAndVersion" — block if missing OR version doesn't satisfy versionCheck. * - "none" — never block, only warn. */ enforcement?: "plugin" | "pluginAndVersion" | "none"; /** * Required plugin version (semver). Only checked when enforcement is * "pluginAndVersion". Setting enforcement to "pluginAndVersion" without * a version is a ManifestError. */ version?: string; /** * How to compare installed version against version. Default: "greaterOrEqual". * - "greaterOrEqual" — installed >= required * - "exact" — installed == required * - "compatible" — same major, installed minor.patch >= required */ versionCheck?: "greaterOrEqual" | "exact" | "compatible";}Behaviour at load time:
Mod dependencies (type: "mod"): the editor topologically sorts mods so each
dependency loads before the mods that need it. If a required mod id isn’t
installed, the dependent mod is blocked with missing dependency "<id>"; a
dependency cycle blocks every mod in the cycle. The version range is recorded
but not enforced in v1 (presence is what’s checked).
Plugin dependencies (type: "plugin"):
- v21+ projects (Plugins/ directory exists): the editor scans
Plugins/*/meta.txtand checks each declared dependency according to itsenforcementsetting:"plugin"(default): blocks the mod if the plugin is missing; version is ignored."pluginAndVersion": blocks the mod if the plugin is missing or its installed version does not satisfy theversionCheckcomparison againstversion. Setting this without aversionis aManifestError."none": never blocks the mod; the editor only logs a warning.
- v16 / BES v5 projects (no Plugins/ directory): the editor logs a console warning but loads the mod anyway, since plugin presence cannot be verified.
The version comparison uses simple semver major.minor.patch (pre-release
suffixes are ignored). The versionCheck field controls the comparison
operator: "greaterOrEqual" (default, installed >= required), "exact"
(installed == required), or "compatible" (same major, installed
minor.patch >= required).
In the Mod Manager’s expanded detail view, plugin dependencies are listed with
clickable links (if url is provided), the enforcement level, and the version
requirement (when applicable).
Multi-File Mod Structure
Mods can split code across multiple .js files using native ES module import:
export function helper() { return 42; }
// index.js (entry — matches manifest "main")import { helper } from './utils.js';
export function activate(ctx) { ctx.ui.showToast({ message: `Answer: ${helper()}`, level: "info" });}How it works: the editor discovers all .js files in your mod folder, builds a dependency graph from import/from specifiers, sorts them topologically (leaf modules first), creates blob URLs bottom-up while rewriting relative imports to blob URLs, then imports your entry module. No manifest changes required — just add .js files alongside your entry.
Rules:
- Only
.jsfiles in the mod directory are discovered (subdirectories included). - Only relative specifiers starting with
./or../are rewritten. Bare specifiers (e.g.import _ from 'lodash') are left as-is and will fail at runtime. - Circular dependencies are handled gracefully (the cycle is broken and modules still load).
- CommonJS mods (
module.exports = ...) remain single-file only — thenew Functionfallback does not support multi-file. - Single-file mods work exactly as before — no changes needed.
Direct Tauri Access
Mods run in the same web context as the editor. When withGlobalTauri
is enabled (it is by default), window.__TAURI__.core.invoke is
available for calling any registered Tauri command — including ones not
exposed through the ctx API.
const invoke = window.__TAURI__.core.invoke;
// Call any registered Tauri commandconst bytes = await invoke("read_binary_file", { path: "/path/to/file" });This is useful when a mod needs capabilities beyond what ctx provides.
See Available Tauri Commands below.
Available Tauri Commands for Mods
These general-purpose Rust commands can be called via
window.__TAURI__.core.invoke. They complement the scoped ctx.fs API
with binary and file-management operations.
File I/O
| Command | Args | Returns | Purpose |
|---|---|---|---|
read_text_file | path | string | Read file as text |
write_text_file | path, content | void | Write text file |
read_binary_file | path | number[] | Read file as raw bytes |
write_binary_file | path, data | void | Write raw bytes |
File Management
| Command | Args | Returns | Purpose |
|---|---|---|---|
list_directory | path | FileEntry[] | List directory entries. Each entry: { name, path, isFile, isDir, size } |
file_exists | path | boolean | Check if a file or directory exists |
copy_file | src, dst | void | Copy file (creates parent dirs) |
rename_file | oldPath, newPath | void | Rename or move a file (creates parent dirs) |
delete_file | path | void | Delete a single file (refuses directories) |
Image
| Command | Args | Returns | Purpose |
|---|---|---|---|
get_image_dimensions | path | [width, height] | Get image dimensions without full decode |
get_tileset_image | gameRoot, tilesetName | number[] (PNG) | Get tileset image as PNG bytes |
get_tileset_info | gameRoot, tilesetName | [w, h] | Tileset image dimensions |
list_autotile_files | gameRoot | string[] | List autotile names in Graphics/Autotiles/ |
list_tileset_files | gameRoot | string[] | List tileset image names in Graphics/Tilesets/ |
list_character_files | gameRoot | string[] | List character sheet names |
list_graphic_files | gameRoot, folder | string[] | List image names (sans extension) in Graphics/<folder>/ — folder must be a single path component (e.g. "Pictures") |
get_graphic_image | gameRoot, folder, name | number[] (PNG) | Get an image from Graphics/<folder>/ as PNG bytes (cached). Same folder validation as list_graphic_files |
clear_image_cache | — | void | Clear the Rust-side image cache |
Tileset Management
| Command | Args | Returns | Purpose |
|---|---|---|---|
create_tileset | gameRoot, name, tilesetName | u32 | Create blank tileset in first nil slot. Returns new tileset ID |
delete_tileset | gameRoot, tilesetId | void | Set tileset slot to nil in Tilesets.rxdata |
update_tileset_name_graphic | gameRoot, tilesetId, name, tilesetName | void | Patch @name/@tileset_name on a tileset |
save_tileset_properties | gameRoot, tilesetId, passages, priorities, terrainTags | void | Patch passage/priority/terrain arrays |
save_expanded_autotiles | gameRoot, tilesetId, expandedAutotiles | void | Patch @expanded_autotiles JSON |
Dialog
| Command | Args | Returns | Purpose |
|---|---|---|---|
plugin:dialog|open | { options: { title?, filters?, multiple?, directory? } } | string | null | Native file/folder picker |
plugin:dialog|save | { options: { defaultPath?, filters? } } | string | null | Native save-file picker |
Note:
plugin:dialog|open/plugin:dialog|saveare Tauri dialog plugin commands — the “plugin” prefix is Tauri’s naming, not the editor’s mod system.
Discord Rich Presence
| Command | Args | Returns | Purpose |
|---|---|---|---|
discord_rpc_connect | appId: string | void | Connect to Discord IPC with your Discord Application ID |
discord_rpc_update | details?, stateText?, largeImage?, largeText?, smallImage?, smallText?, startTimestamp? | void | Set Discord rich presence activity |
discord_rpc_clear | — | void | Clear presence (keep connection open) |
discord_rpc_disconnect | — | void | Close Discord IPC connection |
These commands wrap the discord-rich-presence Rust crate. Connection is managed by the editor backend and cleaned up automatically when the app exits.
Image assets (largeImage, smallImage) must be uploaded in the Discord Developer Portal under Rich Presence → Art Assets. The key passed (e.g. "icon") must match the uploaded asset name.
shadow
shadow.list(mapId): { id, name, visible }[]shadow.setVisible(mapId, shadowId, visible): voidshadow.info(mapId, shadowId): { id, name, visible, opacity } | nullshadow.create(mapId, name?): { id, name }shadow.delete(mapId, shadowId): voidshadow.setOpacity(mapId, shadowId, opacity: number): voidshadow.generateFromTiles( mapId, tiles: Array<{ x, y, tileId, tileData? }>, config?: { blurRadius?, offsetX?, offsetY?, color? }): Promise<{ shadowId: number } | null>setOpacity sets the opacity (0-255) of an existing shadow layer.
generateFromTiles programmatically generates a shadow image from an explicit tile list and appends it as a new shadow layer. Returns the new shadow id, or null if generation failed (e.g. tileset atlas not loaded).
fog
FogCtx is the shared CRUD surface for one graphic layer group — a stack of
image layers drawn above or beneath the map tiles. ctx.fog operates on the
fog group (above tiles, in-game priority 3000); ctx.panorama exposes the
exact same surface on the panorama group (beneath tiles, priority -1000);
mod-registered groups go through ctx.layerGroups below.
fog.list(mapId): { id, name, visible }[]fog.setVisible(mapId, fogId, visible): voidfog.info(mapId, fogId): { id, name, visible, opacity, config } | nullfog.create(mapId, name?): { id, name }fog.delete(mapId, fogId): voidfog.setOpacity(mapId, fogId, opacity: number): voidfog.setConfig(mapId, fogId, config: Partial<PublicFogConfig>): voidconfig shape (PublicFogConfig): { graphicName, hue, blendType, zoom, sx, sy, followPlayer, parallax? }.
| Field | Type | Notes |
|---|---|---|
graphicName | string | Filename in the group’s graphics folder (no extension) — Graphics/Fogs/ for fog, Graphics/Panoramas/ for panorama. |
hue | number | 0-360. |
blendType | number | 0=normal, 1=add, 2=subtract, 3=multiply. |
zoom | number | Scale factor (0.1-8.0). |
sx | number | Horizontal scroll speed (px/frame). Positive = right. |
sy | number | Vertical scroll speed (px/frame). Positive = down. |
followPlayer | boolean | true = screen-locked, false = world-anchored. |
parallax | number? | Camera-follow factor when world-anchored (0..1, default 1): 1 = moves 1:1 with the map (classic fog), 0.5 = RMXP native panorama half-speed, 0 = screen-locked. Ignored while followPlayer is true. |
setConfig merges partial config into the existing config. New fog layers default to opacity 51 (20%) and empty graphic.
panorama
panorama.list(mapId): { id, name, visible }[]panorama.setVisible(mapId, layerId, visible): voidpanorama.info(mapId, layerId): { id, name, visible, opacity, config } | nullpanorama.create(mapId, name?): { id, name }panorama.delete(mapId, layerId): voidpanorama.setOpacity(mapId, layerId, opacity: number): voidpanorama.setConfig(mapId, layerId, config: Partial<PublicFogConfig>): voidSame FogCtx type as ctx.fog, operating on the map’s panorama layers
(drawn beneath the tiles; graphics from Graphics/Panoramas/). create defaults
new layers to opacity 255 and parallax: 0.5 (the classic RMXP panorama scroll
speed). Panorama layers persist in the map file and render in-game via the
MakerStudio plugin; a map that still shows its tileset’s native panorama keeps
it untouched until the panorama layers are first edited.
layerGroups
Register custom graphic layer groups — extra groups like the fog/panorama
ones with a mod-defined render priority. A group is persisted per map inside
@extended_layers (descriptor + layers), so the game renders it even when the
mod is not installed.
layerGroups.register(mapId, def: LayerGroupDef): voidlayerGroups.remove(mapId, key): voidlayerGroups.list(mapId): (LayerGroupDef & { layerCount: number })[]layerGroups.setPriority(mapId, key, priority: number): voidlayerGroups.layers(mapId, key): { id, name, visible, opacity, config }[]layerGroups.addLayer(mapId, key, opts?: { name?, opacity?, config? }): { id, name }layerGroups.deleteLayer(mapId, key, layerId): voidlayerGroups.updateLayer(mapId, key, layerId, patch: { name?, visible?, opacity?, config? }): voidLayerGroupDef: { key, name, priority, folder }.
| Field | Notes |
|---|---|
key | Unique group id. Must not be "fog" or "panorama" (those are the built-in groups). |
name | Display name shown as the group row in the editor’s Layer panel. |
priority | In-game Plane z. < 0 renders beneath the map tiles (panorama is -1000), >= 0 above them (fog is 3000). Groups on the same side render ascending by priority. |
folder | The single Graphics/ subfolder the group’s graphics load from (one path component, e.g. "Pictures"). |
register creates the group on the map or updates its descriptor — existing
layers are preserved when the key already exists. Layers share the fog shape
(PublicFogConfig, including parallax). Group rows appear in the Layer panel
like Fog/Panorama Layers, so map makers can edit the layers you add.
events
events.list(mapId): PublicEvent[]events.get(mapId, eventId): PublicEvent | nullevents.create(mapId, x, y, name?): number | nullevents.delete(mapId, eventId): voidevents.move(mapId, eventId, x, y): voidevents.rename(mapId, eventId, name): voidevents.getFull(mapId, eventId): PublicEventFull | nullevents.update(mapId, event: PublicEventFull): voidPublicEvent = { id, name, x, y, pages?, trigger? }. pages is the event
page count. trigger is the first page trigger type (0=action, 1=player_touch,
2=event_touch, 3=autorun, 4=parallel).
create adds a new event at the given position, returns the event ID.
delete, move, and rename modify existing events. All changes are undoable.
events.commandSchemas(): PublicCommandSchema[]events.getCommandSchema(code: number): PublicCommandSchema | nullevents.createCommand(code: number, params?: unknown[]): { code, indent, parameters }events.validateEvent(event: PublicEventFull): { valid: boolean; errors: string[] }events.registerCommand(def: ModCommandDef): DisposablecommandSchemas() returns all known RMXP event command schemas (code, name, category, defaultParams).
getCommandSchema(code) looks up a single schema by code (returns null for unknown codes).
createCommand(code) builds a valid command struct with default parameters — use when inserting commands into event pages.
validateEvent checks that all command codes in an event’s pages are known. Use before calling events.update.
events.registerCommand(def) — custom event commands
Register a brand-new event command that map makers can insert from the
event-command picker. Your command appears on a dedicated mod page in the
picker (puzzle-icon tabs 🧩1, 🧩2, … after the built-in 1 2 3, 24 commands per page) and
edits through a native declarative form. The form is a builder for a Script
command: filling it in generates plain Ruby that the engine runs directly —
there is no runtime dispatcher.
events.registerCommand(def: ModCommandDef): Disposableinterface ModCommandDef { id: string; // unique within your mod, e.g. "cameraScrollTo" name: string; // shown in the picker + command list page?: string; // reserved (per-mod page titles) fields?: ModCommandField[]; // omit → freeform script textarea (params.script) script?: (params: ModCommandParams) => string; // the Ruby stored & run in-game parse?: (scriptText: string) => ModCommandParams | null; // recover params to re-edit summary?: (params: ModCommandParams) => string; // one-line label in the command list}The registry key is "<modId>:<id>". Duplicate keys are skipped (first wins).
The returned Disposable unregisters the command; it is also auto-removed when
your mod unloads.
Declarative fields (def.fields) render with the editor’s own controls and
selectors, so the dialog matches the built-in command dialogs. Each field’s
key becomes a property on the params object passed to script, parse, and
summary. Any field may set disabled: (params) => boolean to grey its control
out, or hidden: (params) => boolean to remove it entirely, conditionally.
type | Renders | Stored value |
|---|---|---|
number | number box (min/max/step) | number |
text | text input | string |
select | dropdown from options: { value, label }[] | number |
checkbox | checkbox | boolean |
switch | switch picker | switch id (number) |
variable | variable picker | variable id (number) |
coordinate | Transfer-Player-style tile input: a Source dropdown — Direct appointment (map-dialog picker + editable X/Y) or Appoint with variables (X/Y variable pickers). showMapSelector? adds the Map ID dimension + map tree | { mode, mapId, x, y, varMapId, varX, varY } |
record | record picker (recordKind: actor/class/skill/item/weapon/armor/enemy/troop/state/animation/common_event) | record id (number) |
event | event picker (includePlayer?/includeThisEvent?) | event id (number) |
graphic | graphic browser under Graphics/<subfolder>/ (showHue?) | filename (string) |
audio | audio browser (category: BGM/BGS/ME/SE) | { name, volume, pitch } |
Omitting fields gives a freeform script command: the editor shows a single
script textarea bound to params.script.
const off = ctx.events.registerCommand({ id: "cameraScrollTo", name: "Camera Scroll To", fields: [ { type: "coordinate", key: "target", label: "Target tile", showMapSelector: true }, { type: "checkbox", key: "useSpeed", label: "Set speed" }, { type: "number", key: "speed", label: "Speed", min: 1, default: 4, hidden: (p) => !p.useSpeed }, ], script: (p) => { const t = p.target || {}; const cx = t.mode === "variable" ? `$game_variables[${t.varX | 0}]` : (t.x | 0); const cy = t.mode === "variable" ? `$game_variables[${t.varY | 0}]` : (t.y | 0); const args = [cx, cy]; if (p.useSpeed) args.push(p.speed | 0); return `pbCameraScrollTo(${args.join(", ")})`; }, parse: (text) => { const m = /^pbCameraScrollTo\(\s*(-?\d+)\s*,\s*(-?\d+)\s*(?:,\s*(\d+)\s*)?\)$/.exec(text.trim()); return m ? { target: { mode: "direct", mapId: 0, x: +m[1], y: +m[2], varMapId: 1, varX: 1, varY: 1 }, useSpeed: m[3] != null, speed: m[3] != null ? +m[3] : 4 } : null; }, summary: (p) => `(${p.target?.x ?? 0}, ${p.target?.y ?? 0})`,});// off.dispose() to remove it early.How it round-trips & runs. A mod command is stored as an ordinary RMXP
Script command (code 355) whose parameters[0] is exactly the string your
script(params) returns, e.g. pbCameraScrollTo(0, -4). That keeps the map’s
.rxdata round-tripping unchanged, passes validateEvent, and runs in-game like
any other event script — no plugin code or handler is required. parse
recognises a stored script so the command keeps its name in the list and reopens
its form (provide it for re-editability; without it, an inserted command becomes
an ordinary Script command once the dialog closes).
Limitations: values the generated script can’t carry back (e.g. a picked
coordinate mapId, or whether a literal coordinate was picked vs typed)
reset on reopen; there is no imperative custom-render field type yet.
tools.registerTool(def)
ctx.tools.registerTool({ id: "my.tool", label: "My Tool", icon: "★", onActivate() { /* selected from toolbar */ }, onDeactivate() { /* user switched away */ }, onPointerDown(ev) { /* ... */ }, onPointerMove(ev) { /* ... */ }, onPointerUp(ev) { /* ... */ },});Returns a Disposable you can dispose() to unregister early. Otherwise the
tool is removed automatically when the mod unloads.
menu.registerMenuItem(def)
ctx.menu.registerMenuItem({ menu: "Mods", // any existing or new top-level menu label: "Do Something", shortcut: "Ctrl+Shift+D", // registers a real, rebindable binding — do NOT also registerShortcut icon: "database", // optional: built-in icon name, SVG markup, or glyph handler: () => { /* ... */ }, isEnabled: () => true, isChecked: () => false,});If menu matches an existing top-level ("File", "Edit", "View",
"Map", "Tools", "Mods", "Help"), the item is appended to it. If it
matches a submenu label within a top-level menu (e.g. "Import & Export Maps"),
the item is inserted there. Otherwise a new top-level menu is created with
that label.
The optional shortcut (e.g. "Ctrl+Shift+D") registers a real, functional
keyboard binding that fires the item’s handler — it is not just a hint shown
next to the label. The binding appears in the editor’s Keyboard Shortcuts
dialog under a “Mods” section, where the user can rebind it; their override
persists and is reflected in the menu’s displayed shortcut. Because setting
shortcut already binds the key, do not also call
ctx.ui.registerShortcut for the same combo — that double-registers it. (Use
ctx.ui.registerShortcut only for shortcuts that have no menu item.)
The optional icon is rendered in the same style as built-in menu items.
It accepts any of three forms, resolved in order:
- A built-in icon name (kebab-case) — renders the matching editor icon in
the same family as native menu items. Common names:
database,code,save,save-all,grid,layers,undo,redo,cut,copy,paste,import,export,download,zoom-in,zoom-out,select-all,collision,events,dim,lock,unlock,info,mods,keyboard,terminal,run,plus,settings,edit,trash,switch,versions,x,warning,star,refresh,eye,eye-off,chevron-up,chevron-down,chevron-left,chevron-right,arrow-up,arrow-down,arrow-left,arrow-right,pause,stop,step,camera,video,clock,language. (Aliases exist, e.g.recent/history,new/file-plus,scripts/code,dark-mode/moon,add/plus,gear/cog/settings,pencil/rename/edit,delete/remove/trash,swap/repeat/switch,git-branch/branch/versions,globe/translate/language.) - Raw inline SVG markup — any string containing a
<is rendered as-is. - A single unicode glyph — anything else (e.g.
"📊").
commands.register / .execute
ctx.commands.register("mymod.export.xml", async (mapId) => { // ...});
await ctx.commands.execute("mymod.export.xml", 1);Use commands when one mod wants to invoke functionality from another. Command ids are global — namespace them with your mod id.
ui
ctx.ui.registerPanel({ id: "mymod.tilestats", title: "Tile Stats", defaultPosition: "right", showInMenu: true, // default true; set false to hide from Mods menu icon: "📊", // optional: SVG markup or unicode glyph render(host) { host.innerHTML = "<h2>Hello panel</h2>"; return () => { /* optional cleanup */ }; },});
ctx.ui.openPanel("mymod.tilestats");ctx.ui.closePanel("mymod.tilestats");ctx.ui.isPanelOpen("mymod.tilestats"); // → boolean
const value = await ctx.ui.showInputDialog({ title: "Rename", message: "Enter new name:", defaultValue: "old name",});// value is string or null (cancelled)
ctx.ui.showContextMenu(400, 300, [ { label: "Action 1", action: () => { ... } }, { label: "", separator: true }, { label: "Disabled", disabled: true }, { label: "Submenu", submenu: [ { label: "Sub-action", action: () => { ... } }, ]},]);
const ok = await ctx.ui.showConfirmDialog({ title: "Confirm", message: "Sure?" });ctx.ui.showToast({ message: "Done", level: "info" });render(host) is a vanilla DOM callback — use any framework you want
inside, or no framework. Return a cleanup function if you need it.
openPanel(panelId) opens a previously registered panel. closePanel closes
it. isPanelOpen checks visibility. showInMenu: false hides the auto-entry
from the Mods menu (use with your own registerMenuItem instead).
Panel persistence: mod panels keep their slot in the user’s saved dock
layout. When your mod is unloaded (hot reload, disable, uninstall), the panel
is not closed — the editor shows a “Panel provided by mod … — not loaded”
placeholder in its place and swaps your live content back in when the mod
re-registers the panel. Exported layout configurations also record which mods
provided which panels, so importing a layout on a machine missing your mod
warns the user and keeps the slot as a placeholder. Don’t try to close your
panel on deactivate to “clean up” — leaving the slot is the intended
behavior; only the user closes panels.
showInputDialog shows a text input dialog — returns the entered string or
null if cancelled. showContextMenu displays a native context menu at the
given screen coordinates with labels, actions, separators, and submenus.
Theming & CSS variables
Panels (registerPanel) and custom dialogs (showCustomDialog) render into the
editor’s own DOM — not an iframe or shadow root. The editor sets its theme
variables on the <html> element, so they cascade into your UI. Reference
them with var(--name) instead of hardcoding colors, and your panel matches the
app and flips automatically when the user toggles light/dark.
ctx.ui.registerPanel({ id: "mymod.panel", title: "My Panel", render(host) { host.innerHTML = ` <div style=" padding: 8px; background: var(--bg-primary); color: var(--text-primary); font-family: inherit; font-size: 13px;"> <div style=" font-weight: 600; text-transform: uppercase; font-size: 11px; color: var(--text-tertiary); margin-bottom: 6px;">Header</div> <button style=" padding: 4px 10px; border-radius: 4px; cursor: pointer; border: 1px solid var(--border); background: var(--accent); color: var(--accent-text);">OK</button> <button style=" padding: 4px 10px; border-radius: 4px; cursor: pointer; border: 1px solid var(--danger); background: transparent; color: var(--danger);">Delete</button> </div>`; },});Stable palette (same names in both themes — values differ per active theme):
| Variable | Role |
|---|---|
--bg-primary | Main panel background |
--bg-secondary | Sidebar / inset background |
--bg-tertiary | Headers, toolbars, raised controls |
--bg-hover | Hover-state background |
--input-bg | Form input background |
--border | Borders and dividers |
--text-primary | Primary text |
--text-secondary | Labels / secondary text |
--text-tertiary | Muted / placeholder text |
--accent | Accent / primary-action color |
--accent-hover | Accent hover |
--accent-muted | Translucent accent (subtle fills, highlights) |
--accent-text | Text on an accent background |
--danger | Destructive / error |
--warning | Warning |
--highlight | Selection / emphasis |
--success, --success-hover, --success-border | Success states |
--shadow | Drop-shadow color (rgba) |
--canvas-bg | Map canvas backdrop |
The body font is inherited too — set font-family: inherit (13px base) to match.
Notes:
- Treat the names above as the public palette. Other variables exist
(
--ec-*event-command syntax colors,--dv-*Dockview tab tokens,--tile-preview-*) but are internal and may change — don’t depend on them, and don’t override the--dv-*tokens. - CSS variables only apply to DOM. Canvas overlays (
registerOverlay/registerAdvancedOverlay) draw with aCanvasRenderingContext2Dwherevar(--…)does nothing — branch onctx.editor.theme()for a literal color, or read one withgetComputedStyle(document.documentElement).getPropertyValue("--accent").trim().
Context menu registration
ctx.ui.registerContextMenuItem({ context: "map-tile", // "map-tile" | "map-event" | "tile-palette" | "tile-palette-extra" | "layer" | "map-tree" | "event-editor" label: "My Action", handler: (info) => { /* info has mapId, tileX, tileY, tileId, layerIndex, etc. */ }, isEnabled: (info) => true, // optional parentMenu: "Export Map…", // optional — nest inside an existing submenu});Injects menu items into any of 7 editor right-click menus. The info object
contains context-specific data (tile coords, event id, layer name, etc.).
Set parentMenu to an existing submenu label (e.g. "Export Map…" in the
map-tree context menu) to nest your item inside that submenu instead of
appending it at the top level. If no matching submenu is found, the item
is appended at the top level as a fallback.
Overlay rendering
ctx.ui.registerOverlay({ id: "mymod.highlight", zOrder: 0, // higher = on top render(ctx, info) { // ctx = CanvasRenderingContext2D // info = { mapId, tileSize, zoom, viewportX, viewportY, canvasWidth, canvasHeight } ctx.fillStyle = "rgba(255,0,0,0.3)"; ctx.fillRect(x, y, w, h); },});Renders custom overlays on the map canvas after tiles/events, before UI overlays.
Multiple overlays sorted by zOrder.
Keyboard shortcuts
ctx.ui.registerShortcut("Ctrl+Shift+F", () => { // handle shortcut});Register global keyboard shortcuts. Key format: "Ctrl+Shift+F", "Alt+G", etc.
Mod shortcuts take priority over built-in shortcuts.
Additional dialogs and pickers
ctx.ui.showColorPicker(opts?: { title?, initial?: string }): Promise<string | null>ctx.ui.showFilePicker(opts?: { directory?, multiple?, filters? }): Promise<string[] | null>ctx.ui.showSavePicker(opts?: { defaultPath?, filters? }): Promise<string | null>ctx.ui.confirmDestructive(opts: { title, message, confirmLabel? }): Promise<boolean>showColorPicker opens a native color picker; returns hex string (e.g. "#ff0000") or null if dismissed.
showFilePicker wraps the Tauri file-open dialog; returns array of paths or null.
showSavePicker wraps the Tauri save dialog; returns path or null.
confirmDestructive is showConfirmDialog with a red confirm button — use for irreversible operations.
Custom dialogs
const { close } = ctx.ui.showCustomDialog({ title: "My Dialog", width: "460px", // optional height: "400px", // optional zIndex: 5000, // optional render(body) { // body is an HTMLElement — append your UI const btn = document.createElement("button"); btn.textContent = "Close"; btn.addEventListener("click", () => close()); body.appendChild(btn);
// Return cleanup function (optional) return () => { /* teardown */ }; },});Opens a styled modal dialog using the editor’s standard shell (overlay, draggable
header with title + close button, scrollable body). The render callback receives
the body element — append any DOM content. Returns { close } to close programmatically.
Advanced overlay
ctx.ui.registerAdvancedOverlay({ id: "mymod.overlay", zOrder: 0, render(ctx, info) { // info.animFrame — coarse frame counter for animation ctx.fillStyle = "rgba(255,0,0,0.3)"; ctx.fillRect(x, y, w, h); },}): DisposableLike registerOverlay but info includes animFrame for animation-aware rendering.
Status bar and toolbar (registry — rendering coming soon)
ctx.ui.registerStatusBarItem({ id, render(host), align?: "left"|"right" }): Disposablectx.ui.registerToolbarButton({ id, icon, tooltip, handler, isActive? }): DisposableRegisters a status bar segment or toolbar button contributed by the mod. Both are removed automatically on mod unload.
Opening URLs
await ctx.ui.openUrl("https://example.com");Opens a URL in the user’s default browser. The editor shows a confirmation dialog first (with the URL displayed in a monospace box) — the user must click “Open Link” to proceed. Returns immediately if cancelled.
keybinds
keybinds.list(): KeybindInfo[]keybinds.get(actionId: string): KeybindInfo | nullkeybinds.set(actionId: string, key: string): string | nullkeybinds.reset(actionId: string): voidkeybinds.onChanged(cb: (actionId, oldKey, newKey) => void): DisposableQuery and modify the editor’s keyboard shortcuts. KeybindInfo contains actionId, label, category, key, defaultKey, and isCustom.
set returns the conflicting action ID if the key is already bound, or null on success. The key format uses normalized notation: "ctrl+s", "ctrl+shift+s", "g", etc.
onChanged subscribes to any keybind change (from the settings dialog or other mods). Returns a Disposable.
Built-in action IDs include: tool.brush, tool.eraser, tool.fill, tool.rectangle, tool.eyedropper, tool.select, tool.pan, view.toggleGrid, view.toggleCollision, view.toggleDim, brush.sizeUp, brush.sizeDown, brush.rotateCW, brush.rotateCCW, zoom.in, zoom.out, layer.select1–layer.select9, edit.save, edit.saveAll, edit.saveShadow, edit.undo, edit.redo, edit.selectAll, edit.deselect, edit.copy, edit.paste, edit.cut, app.runGame, dev.toggleDevTools. Call ctx.keybinds.list() for the full set.
Lifecycle
ctx.lifecycle.onUndo(fn: (mapId, label) => void): Disposablectx.lifecycle.onRedo(fn: (mapId, label) => void): Disposablectx.lifecycle.onBrushChange(fn: (props) => void): Disposablectx.lifecycle.onTilesetChange(fn: (tilesetId, reason) => void): DisposableConvenience wrappers around bus.on("undo"), bus.on("redo"), bus.on("brush.changed"), bus.on("tileset.changed").
i18n
Translations and locale control. Two tiers:
- Tier 1 — translate your own strings. Register per-locale dictionaries for your mod’s UI,
then resolve them with the scoped
i18n.t(). - Tier 2 — localize the whole app. Register a complete new locale — it appears in
View → Language — or patch an existing one (including the built-in
en/es).
i18n.addTranslations(locale: string, dict: Record<string, string>): Disposablei18n.t(source: string, vars?: Record<string, string | number>): stringi18n.getLocale(): stringi18n.locales(): LocaleInfo[] // [{ code, name }] — built-ins + mod localesi18n.setLocale(code: string): voidi18n.registerLocale(loc: { code: string; name: string; dict: Record<string, string> }): Disposablei18n.onChanged(cb: (locale: string) => void): Disposable// Tier 1 — your mod's own stringsctx.i18n.addTranslations("es", { "Export finished": "Exportación completada", "Exported {n} maps": "Se exportaron {n} mapas",});ctx.ui.showToast({ message: ctx.i18n.t("Exported {n} maps", { n: 3 }) });
// Tier 2 — a whole new app locale (shows up in View → Language)ctx.i18n.registerLocale({ code: "fr", name: "Français", dict: frenchDict });
// React to language switchesctx.i18n.onChanged((locale) => rerenderMyPanel(locale));Details:
- Keys are English source strings (gettext style), same convention as the app’s own
dictionaries.
i18n.t()looks up your mod’s dict for the current locale, then the app dictionary, then falls back to the source string itself — untranslated strings are never broken, just English.{name}placeholders are substituted fromvarsafter lookup, so dictionary keys keep the literal braces. addTranslationsmerges — calling it again for the same locale adds/overrides keys. The returnedDisposableremoves exactly the keys that call added.registerLocaleoverlays the app’st()for that locale and lists it in View → Language. Reusing a built-in code (e.g."es") patches that locale’s app strings. Mod entries win over built-ins; among mods, the later registration wins.- If a user’s saved language comes from your mod, it is restored automatically when your mod (re)registers the locale — including across hot reloads. While the mod is unloaded the editor falls back to its detected default.
- Everything is auto-disposed on unload/hot-reload.
- Locale switches fire the
"locale.changed"bus event ({ locale }) —i18n.onChangedis the convenience wrapper. See events-reference.md.
bus
const sub = ctx.bus.on("map.tile.changed", (e) => { ... });sub.dispose(); // optional — happens automatically on unload
ctx.bus.emit("mod.loaded", { id: "..." }); // editor-level eventssave.before is cancellable: return { cancel: true, reason: "..." } to
abort the save.
See events-reference.md for the full list.
fs
// Mod folder operationsconst txt = await ctx.fs.readModFile("data.txt");await ctx.fs.writeModFile("output.txt", "hello");const exists = await ctx.fs.exists("data.txt");const entries = await ctx.fs.listDir("subfolder/");await ctx.fs.mkdir("output/cache");
// Project folder operationsconst projTxt = await ctx.fs.readProjectFile("Data/System.rxdata");await ctx.fs.writeProjectFile("Data/custom.json", content);const projExists = await ctx.fs.projectExists("Graphics/Tilesets/my_tile.png");const projEntries = await ctx.fs.listProjectDir("Graphics/Tilesets/");await ctx.fs.projectMkdir("Graphics/Custom");Paths are normalized — absolute paths and .. traversal raise
PermissionDeniedError.
exists/listDir/mkdir operate in the mod folder. projectExists/
listProjectDir/projectMkdir operate in the project (game) folder and
require fs.project or fs.write.project permissions.
storage
await ctx.storage.set("counter", 1);const v = await ctx.storage.get<number>("counter", 0);Backed by <modDir>/.storage.json.
clipboard
The OS text clipboard — the system-wide clipboard shared with every other application.
clipboard.readText(): Promise<string | null>clipboard.writeText(text: string): Promise<void>await ctx.clipboard.writeText("hello");const text = await ctx.clipboard.readText(); // null if empty / unavailablereadText returns whatever raw text the OS clipboard holds — which may be a copy made by
another app — or null when empty or the clipboard is unavailable (permission / headless).
writeText is a no-op when the clipboard is unavailable; neither method throws.
This is not the tile clipboard: ctx.map.getClipboard() / setClipboard() hold copied
map tiles, fire clipboard.changed, and never touch the OS clipboard.
log
ctx.log.info("hello", { foo: 1 });ctx.log.warn(...);ctx.log.error(err);Visible in the Mod Manager log pane.
lifecycle
ctx.lifecycle.onMapLoad((mapId) => { ... });ctx.lifecycle.onSave((mapId) => { ... });ctx.lifecycle.onActivate(() => { /* runs after activate() returns */ });ctx.lifecycle.onDeactivate(() => { /* runs on mod unload */ });ctx.lifecycle.onToolChange((toolId) => { ... });ctx.lifecycle.onLayerChange((mapId, layerIndex) => { ... });Convenience wrappers around bus.on(...) for common hooks.
onToolChange fires on tool.activated. onLayerChange fires on
layer.changed (visibility, opacity, added, removed, active).
stats
Read editor usage statistics and define custom stats.
Snapshot getters
const g = ctx.stats.global(); // GlobalStatsSnapshotconst p = ctx.stats.project(); // ProjectStatsSnapshot | nullconst a = ctx.stats.all(); // CombinedStatsSnapshot { global, project }GlobalStatsSnapshot fields: totalActiveMinutes, totalTilesPlaced, totalUndoCount, totalRedoCount, totalMapsCreated, totalMapsSaved, totalSessions, firstLaunchDate, custom.
ProjectStatsSnapshot fields: activeMinutes, tilesPlaced, undoCount, redoCount, mapsCreated, mapsSaved, mapEdits, sessionCount, firstOpened, custom.
Single-stat getters
const tiles = ctx.stats.getGlobalStat("totalTilesPlaced"); // number | stringconst pTiles = ctx.stats.getProjectStat("tilesPlaced"); // number | string | nullCustom stats
Mods can define arbitrary numeric stats that persist alongside built-in stats:
// Readconst v = ctx.stats.getCustomGlobal("my_counter", 0);const pv = ctx.stats.getCustomProject("my_project_counter", 0);
// Writectx.stats.setCustomGlobal("my_counter", 42);ctx.stats.setCustomProject("my_project_counter", 10);
// Increment (returns new value)const newV = ctx.stats.incrementCustomGlobal("my_counter"); // +1const newP = ctx.stats.incrementCustomProject("my_counter", 5); // +5Custom stats appear in the Stats dialog under a “Custom” section. Project custom stats are per-project; global custom stats are shared across all projects.
Stat metadata
Mods can register display metadata for custom stats so they appear with a name, description, and category in the Stats dialog:
ctx.stats.registerStat({ id: "achievements_unlocked", // matches the key used in setCustomGlobal/setCustomProject name: "Achievements Unlocked", // display name shown in the Stats dialog description: "Total achievements unlocked across all projects", // shown on hover category: "Achievements", // section header in the Stats dialog scope: "global", // "global" or "project" format: "number", // optional: "number" (default) | "time" | "date"});Stats with the same category are grouped under a single heading. Built-in stats use “Project” and “Global” categories. Mods can reuse these or create custom categories.
Subscription
ctx.stats.onStatsChanged((global, project) => { // Fires ~every 60s with fresh snapshots});