Editor plugins
A plugin is Odin code that adds to the editor itself, for your project: a command that lays out
a level, an importer that turns your spreadsheet into dialogue, inspector buttons for the nodes
your game uses most. Plugins live in the project (plugins/), so everyone working on it gets them;
the editor builds and loads them, and builds them again whenever you save.
A plugin can add:
| What | Where it shows | Example |
|---|---|---|
| Commands | buttons in the bottom panel’s Plugins tab | “Arrange Children in a Grid”, “Snap Everything to Pixels” |
| Importers | importing files of its extensions (Ctrl+I) | .csv lines into a dialogue, a level format into a scene |
| Inspector rows | the inspector, for nodes of some kinds or scripts | a note, a number, a checkbox, a button on every Sprite |
Plugins need Odin installed, like scripts do (Settings > Toolchain).
- Your first plugin
- What a plugin looks like
- The Host: reading and changing the project
- Commands
- Importers
- Inspector rows
- How plugins are built and loaded
Your first plugin
Section titled “Your first plugin”- Open the bottom panel’s Plugins tab and press New Plugin. The editor writes
plugins/my_plugin/my_plugin.odin(a command and inspector rows to start from) and opens it in a code tab. - A moment later the tab shows it Loaded, with its Count Nodes button. Press it: the Console says how many nodes the scene has.
- Select a Sprite: the inspector has a my_plugin section with its picture and a Flip button.
- Change the code and save (Ctrl+S): the editor builds it again and swaps the new one in. Build errors go to the Output tab, and the Plugins tab says it failed.
Rename the folder and the package line to name your plugin; every folder in plugins/ is a
plugin.
What a plugin looks like
Section titled “What a plugin looks like”package grid_tools
import "core:fmt"import api "plugin_api:."
// What it adds: package-level lists (a list written inside the procedure below would be gone// when it returns)commands := []api.Command{ {name = "Arrange Children in a Grid", description = "Lays the selected node's children out in rows of four", run = arrange},}
// The editor calls this when it loads the plugin@(export)crossfire_plugin :: proc(host: ^api.Host) -> api.Plugin { return { name = "Grid Tools", description = "Arranges nodes on a grid", version = api.PLUGIN_API_VERSION, commands = commands, }}
arrange :: proc(host: ^api.Host) { parent := host.selected() if parent < 0 { host.log("Select a node whose children to arrange", warning = true) return } for child, i in host.nodeChildren(parent) { host.setProperty(child, "position", fmt.tprintf("%d, %d", (i % 4) * 48, (i / 4) * 48)) }}- The package imports
plugin_api:.(the editor passes that collection when it builds). Its types arePlugin,Command,Importer,Inspector,InspectorRowandHost. crossfire_pluginmust be@(export)ed and return the plugin withversion = api.PLUGIN_API_VERSION(a plugin built for another version isn’t loaded).- It can use Odin’s core library (
core:osfor files,core:strings,core:encoding/json…). It doesn’t use the engine: it changes the project through the Host. - Its package variables keep their values while it’s loaded (a setting it remembers), and start over when it’s rebuilt.
- Use
context.temp_allocatorfor anything temporary: the editor’s frame memory.
The Host: reading and changing the project
Section titled “The Host: reading and changing the project”Every procedure a plugin gives gets the host. Nodes are numbered in the open scene (or
component), in scene tree order; -1 means none (as a parent: the top level).
| Host procedure | Does |
|---|---|
log(text, warning = false) |
a line in the Console |
projectRoot() |
the project’s folder |
sceneName(), nodeCount() |
the open scene |
nodeParent(node), nodeChildren(node) |
its tree |
getProperty(node, property), setProperty(node, property, value) |
a node’s properties, as text |
addNode(kind, name, parent), removeNode(node) |
adding and removing nodes |
selected(), selection(), select(node) |
the selection |
assets(), assetKind(path) |
the project’s assets |
addAsset(path) |
lists a file inside the project folder as an asset |
Properties are written the way the inspector shows them: "name", "kind" (read only),
"position" and "scale" ("x, y"), "rotation", "visible" ("true"), "layer",
"z_index", "texture", "text", "color" ("#ff8000"), "region", "script" (read only), and a
script’s fields as "script.speed".
Commands
Section titled “Commands”countNodes :: proc(host: ^api.Host) { host.log(fmt.tprintf("The scene has %d nodes", host.nodeCount()))}
commands := []api.Command{ {name = "Count Nodes", description = "Logs how many nodes the scene has", run = countNodes},}Each command is a button in the Plugins tab (its description is the button’s tooltip). Whatever a command changes in the project is one undo step: Ctrl+Z puts it all back.
Importers
Section titled “Importers”importCSV :: proc(host: ^api.Host, source: string) -> (asset: string, problem: string) { // Read `source` (the file the user picked), write what the project needs inside its folder, // list it with host.addAsset, and return the asset to select // ... return "dialogues/talk.dialogue.xml", ""}
importers := []api.Importer{ {extensions = {"csv"}, description = "speaker,text lines into a dialogue", run = importCSV},}When the user imports a file whose extension a loaded plugin takes, the plugin’s importer runs
instead of the editor’s own. Return problem (and no asset) when the file can’t be imported: the
Console shows it.
Inspector rows
Section titled “Inspector rows”inspectors := []api.Inspector{ {targets = {"Sprite", "script:Enemy"}, rows = spriteRows, changed = spriteChanged},}
spriteRows :: proc(host: ^api.Host, node: int) -> []api.InspectorRow { rows := make([]api.InspectorRow, 2, context.temp_allocator) rows[0] = {label = "Picture", kind = .Note, value = "..."} rows[1] = {id = "snap", label = "Snap", kind = .Button, value = "Snap to the Grid"} return rows}
spriteChanged :: proc(host: ^api.Host, node: int, id, value: string) { if id == "snap" { // Pressed: change the node through the host }}- targets: node kinds (
"Sprite","Label", a node type of your code) and scripts ("script:Enemy": nodes running that script). - rows is asked every time the inspector draws: only read here. Row kinds:
.Note(text to read),.Text,.Number(fields),.Bool(a checkbox:"true"/"false"),.Choice(a dropdown ofchoices),.Button(valueis its text). - changed is called with the row’s
idand new value (a button:""); its changes are an undo step.
How plugins are built and loaded
Section titled “How plugins are built and loaded”- While the project has a
plugins/folder, the editor looks at it twice a second. A plugin whose.odinfiles are newer than its last build is built (odin build plugins/<name> -build-mode:shared) into the editor’sbuild/plugins/folder, a new file each time, and loaded in place of the old one. One builds at a time; the others wait. - The Plugins tab lists each one: Waiting, Building, Loaded or Failed (and why). Rebuild All builds them all again.
- Plugins run inside the editor, with its rights: only use plugins you trust, like any code in the project.
- Exported games don’t contain plugins: they’re for the editor only.
Questions
Section titled “Questions”My plugin says Failed. The Output tab has the compiler’s errors. “exports no crossfire_plugin
procedure”: add @(export). “made for plugin API version 0”: set version = api.PLUGIN_API_VERSION.
It says “It is unsafe to return a compound literal of a slice”. Declare the lists at package
level (commands := []api.Command{...}), not inside crossfire_plugin.
My inspector rows don’t show. Check the target spells the kind as the scene tree does
("Sprite", "AnimatedSprite") and the plugin is Loaded.
