Ir al contenido

API Reference

Esta página aún no está disponible en tu idioma.

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.

FieldTypePurpose
apiVersionstringEchoes the editor’s API version.
manifestReadonly<Manifest>This mod’s manifest.
editorEditorCtxActive map / layer / tool state.
mapMapCtxRead and write tiles, query layers.
tilesetTilesetCtxTileset images and tile properties.
shadowShadowCtxShadow layer queries.
fogFogCtxFog layer queries and config (above-tile graphic layer group).
panoramaFogCtxPanorama layers — same surface as fog, beneath the tiles.
layerGroupsLayerGroupsCtxCustom graphic layer groups with mod-defined priorities.
eventsEventsCtxRMXP-style events on a map.
toolsToolsCtxRegister custom editing tools.
menuMenuCtxAdd menu items.
commandsCommandsCtxCross-mod command registry.
uiUiCtxPanels, dialogs, toasts.
busBusCtxSubscribe to / emit events.
fsFsCtxPath-scoped filesystem.
storageStorageCtxPer-mod persistent K/V.
clipboardClipboardCtxOS text clipboard read/write (not the tile clipboard).
statsStatsCtxRead editor usage statistics.
keybindsKeybindsCtxQuery and modify keyboard shortcuts.
i18nI18nCtxTranslate the mod’s own strings, register whole app locales.
selectorsSelectorsCtxModal pickers (actor, item, switch, map, tileset, audio, graphic, …).
projectDataProjectDataCtxRead-only RPG record lists (actors, items, switches, …).
modsModsCtxQuery other installed mods (presence, status).
pluginsPluginsCtxQuery installed Essentials plugins.
logLogCtxNamespaced logger.
lifecycleLifecycleCtxActivation hooks.

editor

editor.version(): string
editor.activeMapId(): number | null
editor.activeLayerIndex(): number
editor.activeTool(): string
editor.setTool(toolId: string): void
editor.setActiveLayer(index: number): void
editor.listOpenMaps(): number[]
editor.gameRoot(): string | null
editor.brushSize(): number
editor.setBrushSize(size: number): void
editor.brushTileProperties(): { rotation, flipH, flipV, opacity, hue, saturation, lighting }
editor.setBrushTileProperties(props: Partial<{...}>): void
editor.hoverTile(): { x, y } | null
editor.viewport(): { x, y, zoom }
editor.setViewport(viewport: { x?, y?, zoom? }): void
editor.viewOptions(): { showGrid, showCollision, showEvents, showDim, darkMode }
editor.setViewOptions(opts: Partial<{...}>): void

Read/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(): number
editor.requestRedraw(): void
editor.setStatusBarText(text: string | null): void
editor.recentMaps(): number[]
editor.projectName(): string | null

theme() 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 | null
map.layers(mapId): PublicLayerInfo[]
map.readTile(mapId, layer, x, y): number
map.writeTile(mapId, layer, x, y, tileId, label?): void
map.batchWrite(mapId, layer, tiles: Map<"x,y", tileId>, label): void
map.selection(mapId): { bounds, count }
map.readTileData(mapId, layer, x, y): PublicTileData | null
map.writeTileData(mapId, layer, x, y, data, label?): void
map.selectionTiles(mapId): Array<{ x, y, tileId, layerIndex }> | null
map.setSelection(mapId, tiles: Array<{ x, y }> | null): void
map.addLayer(mapId, name?): number
map.removeLayer(mapId, layerIndex): void
map.renameLayer(mapId, layerIndex, name): void
map.setLayerVisible(mapId, layerIndex, visible): void
map.setLayerOpacity(mapId, layerIndex, opacity): void
map.createMap(opts: { width?, height?, tilesetId?, name?, parentId? }): Promise<number>
map.deleteMap(mapId): Promise<void>
map.resize(mapId, newWidth, newHeight, shiftX?, shiftY?): void
map.renameMap(mapId, name): void
map.reparentMap(mapId, parentId): Promise<void>
map.beginUndoGroup(label): void
map.endUndoGroup(): void

writeTile 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? };
}): void
map.recalculateAutotiles(mapId, layerIndex, tiles: Array<{x,y}>): void
map.getNativeTileProperties(mapId, layer, x, y): NativeTileProps | null
map.setNativeTileProperties(mapId, layer, x, y, props: Partial<NativeTileProps>): void
map.getClipboard(): PublicClipboardData | null
map.setClipboard(data: PublicClipboardData | null): void
map.createUndoScope(mapId, label): UndoScope

transformSelection 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 } | null
tileset.list(): Array<{ id, name, tilesetName }>
tileset.info(tilesetId): { id, name, tilesetName, autotileNames, panoramaName, fogName, battlebackName } | null
tileset.setTileProperties(tilesetId, tileId, { passage?, priority?, terrainTag? }): Promise<void>
tileset.currentId(): number | null
tileset.current(): { id, name, tilesetName, autotileNames, panoramaName, fogName, battlebackName } | null
tileset.mapTilesetId(mapId): number | null

The 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 } | null
tileset.resolveTileProperties(tileId, tilesetId?): { passage, priority, terrainTag } | null

transformTileProperties 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 }): Disposable
tileset.registerPriority({ id, name }): Disposable

registerTerrainTag / 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>
// System
selectors.pickSwitch(opts?): Promise<EntityPickResult | null>
selectors.pickVariable(opts?): Promise<EntityPickResult | null>
// Map / event / tileset
selectors.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-backed
selectors.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>
// Input
selectors.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 it
const 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 slider
const 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 volume
const 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 one
mods.get(id: string): InstalledModInfo | null
mods.isInstalled(id: string): boolean // present in any status
mods.isActive(id: string): boolean // present AND loaded/running
interface 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 available
plugins.get(name: string): InstalledPluginInfo | null
plugins.isInstalled(name: string): boolean
interface 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.txt and checks each declared dependency according to its enforcement setting:
    • "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 the versionCheck comparison against version. Setting this without a version is a ManifestError.
    • "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:

utils.js
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 .js files 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 — the new Function fallback 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 command
const 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

CommandArgsReturnsPurpose
read_text_filepathstringRead file as text
write_text_filepath, contentvoidWrite text file
read_binary_filepathnumber[]Read file as raw bytes
write_binary_filepath, datavoidWrite raw bytes

File Management

CommandArgsReturnsPurpose
list_directorypathFileEntry[]List directory entries. Each entry: { name, path, isFile, isDir, size }
file_existspathbooleanCheck if a file or directory exists
copy_filesrc, dstvoidCopy file (creates parent dirs)
rename_fileoldPath, newPathvoidRename or move a file (creates parent dirs)
delete_filepathvoidDelete a single file (refuses directories)

Image

CommandArgsReturnsPurpose
get_image_dimensionspath[width, height]Get image dimensions without full decode
get_tileset_imagegameRoot, tilesetNamenumber[] (PNG)Get tileset image as PNG bytes
get_tileset_infogameRoot, tilesetName[w, h]Tileset image dimensions
list_autotile_filesgameRootstring[]List autotile names in Graphics/Autotiles/
list_tileset_filesgameRootstring[]List tileset image names in Graphics/Tilesets/
list_character_filesgameRootstring[]List character sheet names
list_graphic_filesgameRoot, folderstring[]List image names (sans extension) in Graphics/<folder>/folder must be a single path component (e.g. "Pictures")
get_graphic_imagegameRoot, folder, namenumber[] (PNG)Get an image from Graphics/<folder>/ as PNG bytes (cached). Same folder validation as list_graphic_files
clear_image_cachevoidClear the Rust-side image cache

Tileset Management

CommandArgsReturnsPurpose
create_tilesetgameRoot, name, tilesetNameu32Create blank tileset in first nil slot. Returns new tileset ID
delete_tilesetgameRoot, tilesetIdvoidSet tileset slot to nil in Tilesets.rxdata
update_tileset_name_graphicgameRoot, tilesetId, name, tilesetNamevoidPatch @name/@tileset_name on a tileset
save_tileset_propertiesgameRoot, tilesetId, passages, priorities, terrainTagsvoidPatch passage/priority/terrain arrays
save_expanded_autotilesgameRoot, tilesetId, expandedAutotilesvoidPatch @expanded_autotiles JSON

Dialog

CommandArgsReturnsPurpose
plugin:dialog|open{ options: { title?, filters?, multiple?, directory? } }string | nullNative file/folder picker
plugin:dialog|save{ options: { defaultPath?, filters? } }string | nullNative save-file picker

Note: plugin:dialog|open / plugin:dialog|save are Tauri dialog plugin commands — the “plugin” prefix is Tauri’s naming, not the editor’s mod system.

Discord Rich Presence

CommandArgsReturnsPurpose
discord_rpc_connectappId: stringvoidConnect to Discord IPC with your Discord Application ID
discord_rpc_updatedetails?, stateText?, largeImage?, largeText?, smallImage?, smallText?, startTimestamp?voidSet Discord rich presence activity
discord_rpc_clearvoidClear presence (keep connection open)
discord_rpc_disconnectvoidClose 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): void
shadow.info(mapId, shadowId): { id, name, visible, opacity } | null
shadow.create(mapId, name?): { id, name }
shadow.delete(mapId, shadowId): void
shadow.setOpacity(mapId, shadowId, opacity: number): void
shadow.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): void
fog.info(mapId, fogId): { id, name, visible, opacity, config } | null
fog.create(mapId, name?): { id, name }
fog.delete(mapId, fogId): void
fog.setOpacity(mapId, fogId, opacity: number): void
fog.setConfig(mapId, fogId, config: Partial<PublicFogConfig>): void

config shape (PublicFogConfig): { graphicName, hue, blendType, zoom, sx, sy, followPlayer, parallax? }.

FieldTypeNotes
graphicNamestringFilename in the group’s graphics folder (no extension) — Graphics/Fogs/ for fog, Graphics/Panoramas/ for panorama.
huenumber0-360.
blendTypenumber0=normal, 1=add, 2=subtract, 3=multiply.
zoomnumberScale factor (0.1-8.0).
sxnumberHorizontal scroll speed (px/frame). Positive = right.
synumberVertical scroll speed (px/frame). Positive = down.
followPlayerbooleantrue = screen-locked, false = world-anchored.
parallaxnumber?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): void
panorama.info(mapId, layerId): { id, name, visible, opacity, config } | null
panorama.create(mapId, name?): { id, name }
panorama.delete(mapId, layerId): void
panorama.setOpacity(mapId, layerId, opacity: number): void
panorama.setConfig(mapId, layerId, config: Partial<PublicFogConfig>): void

Same 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): void
layerGroups.remove(mapId, key): void
layerGroups.list(mapId): (LayerGroupDef & { layerCount: number })[]
layerGroups.setPriority(mapId, key, priority: number): void
layerGroups.layers(mapId, key): { id, name, visible, opacity, config }[]
layerGroups.addLayer(mapId, key, opts?: { name?, opacity?, config? }): { id, name }
layerGroups.deleteLayer(mapId, key, layerId): void
layerGroups.updateLayer(mapId, key, layerId, patch: { name?, visible?, opacity?, config? }): void

LayerGroupDef: { key, name, priority, folder }.

FieldNotes
keyUnique group id. Must not be "fog" or "panorama" (those are the built-in groups).
nameDisplay name shown as the group row in the editor’s Layer panel.
priorityIn-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.
folderThe 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 | null
events.create(mapId, x, y, name?): number | null
events.delete(mapId, eventId): void
events.move(mapId, eventId, x, y): void
events.rename(mapId, eventId, name): void
events.getFull(mapId, eventId): PublicEventFull | null
events.update(mapId, event: PublicEventFull): void

PublicEvent = { 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 | null
events.createCommand(code: number, params?: unknown[]): { code, indent, parameters }
events.validateEvent(event: PublicEventFull): { valid: boolean; errors: string[] }
events.registerCommand(def: ModCommandDef): Disposable

commandSchemas() 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): Disposable
interface 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.

typeRendersStored value
numbernumber box (min/max/step)number
texttext inputstring
selectdropdown from options: { value, label }[]number
checkboxcheckboxboolean
switchswitch pickerswitch id (number)
variablevariable pickervariable id (number)
coordinateTransfer-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 }
recordrecord picker (recordKind: actor/class/skill/item/weapon/armor/enemy/troop/state/animation/common_event)record id (number)
eventevent picker (includePlayer?/includeThisEvent?)event id (number)
graphicgraphic browser under Graphics/<subfolder>/ (showHue?)filename (string)
audioaudio 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.


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:

  1. 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.)
  2. Raw inline SVG markup — any string containing a < is rendered as-is.
  3. 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):

VariableRole
--bg-primaryMain panel background
--bg-secondarySidebar / inset background
--bg-tertiaryHeaders, toolbars, raised controls
--bg-hoverHover-state background
--input-bgForm input background
--borderBorders and dividers
--text-primaryPrimary text
--text-secondaryLabels / secondary text
--text-tertiaryMuted / placeholder text
--accentAccent / primary-action color
--accent-hoverAccent hover
--accent-mutedTranslucent accent (subtle fills, highlights)
--accent-textText on an accent background
--dangerDestructive / error
--warningWarning
--highlightSelection / emphasis
--success, --success-hover, --success-borderSuccess states
--shadowDrop-shadow color (rgba)
--canvas-bgMap 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 a CanvasRenderingContext2D where var(--…) does nothing — branch on ctx.editor.theme() for a literal color, or read one with getComputedStyle(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);
},
}): Disposable

Like 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" }): Disposable
ctx.ui.registerToolbarButton({ id, icon, tooltip, handler, isActive? }): Disposable

Registers 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 | null
keybinds.set(actionId: string, key: string): string | null
keybinds.reset(actionId: string): void
keybinds.onChanged(cb: (actionId, oldKey, newKey) => void): Disposable

Query 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.select1layer.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): Disposable
ctx.lifecycle.onRedo(fn: (mapId, label) => void): Disposable
ctx.lifecycle.onBrushChange(fn: (props) => void): Disposable
ctx.lifecycle.onTilesetChange(fn: (tilesetId, reason) => void): Disposable

Convenience 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>): Disposable
i18n.t(source: string, vars?: Record<string, string | number>): string
i18n.getLocale(): string
i18n.locales(): LocaleInfo[] // [{ code, name }] — built-ins + mod locales
i18n.setLocale(code: string): void
i18n.registerLocale(loc: { code: string; name: string; dict: Record<string, string> }): Disposable
i18n.onChanged(cb: (locale: string) => void): Disposable
// Tier 1 — your mod's own strings
ctx.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 switches
ctx.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 from vars after lookup, so dictionary keys keep the literal braces.
  • addTranslations merges — calling it again for the same locale adds/overrides keys. The returned Disposable removes exactly the keys that call added.
  • registerLocale overlays the app’s t() 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.onChanged is 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 events

save.before is cancellable: return { cancel: true, reason: "..." } to abort the save.

See events-reference.md for the full list.


fs

// Mod folder operations
const 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 operations
const 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 / unavailable

readText 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(); // GlobalStatsSnapshot
const p = ctx.stats.project(); // ProjectStatsSnapshot | null
const 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 | string
const pTiles = ctx.stats.getProjectStat("tilesPlaced"); // number | string | null

Custom stats

Mods can define arbitrary numeric stats that persist alongside built-in stats:

// Read
const v = ctx.stats.getCustomGlobal("my_counter", 0);
const pv = ctx.stats.getCustomProject("my_project_counter", 0);
// Write
ctx.stats.setCustomGlobal("my_counter", 42);
ctx.stats.setCustomProject("my_project_counter", 10);
// Increment (returns new value)
const newV = ctx.stats.incrementCustomGlobal("my_counter"); // +1
const newP = ctx.stats.incrementCustomProject("my_counter", 5); // +5

Custom 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
});