Extend the editor
Add a plugin panel that edits the selected entity with undo support, then expose commands or use host services for scene and project operations.
Prepare an editor plugin
Use a registered plugin with an editor panel. The
hook reference lists current signatures and
call order. Editor hooks receive a borrowed *eggy.editor.Context, available only
during the hook. Do not store it, App pointers, or live component pointers for a later
call. editor.app is the same App that game hooks receive.
Scene and file mutations require editing mode. Selection is available in both editing
mode and Play. List an edited component in the plugin's components so scenes can
store it; pointer-containing components cannot be snapshotted with changeComponent.
Custom importer, inspector, and scene-tool registration currently return
error.NotImplemented. They do not retain callbacks or register an extension.
Weeoui's custom widgets and layouts remain usable in panels.
Edit the selected entity
In editorPanelUi, copy the current value, edit the copy, and pass both values to
changeComponent:
pub fn editorPanelUi(
self: *Plugin,
panel: usize,
editor: *eggy.editor.Context,
ui: *eggy.ui.Context,
) !void {
_ = self;
_ = panel;
if (!ui.button("Raise selection")) return;
const entity = try editor.selection() orelse return;
const transform = editor.app.world.get(entity, eggy.Transform) orelse return;
var after = transform.*;
after.position[1] += 1;
try editor.changeComponent(entity, eggy.Action(eggy.Transform){
.before = transform.*,
.after = after,
});
}
changeComponent validates the entity, checks the expected before-state, applies the value,
marks the component changed, and records the scene in host-owned history. Do not also
assign the component or call markChanged. A value that has no serialized scene
change is rejected and restored.
Fields are preflighted against the existing 4 KiB serialized-field and 16 KiB parse budgets.
Oversized or over-complex values fail before mutation, which prevents an incomplete undo.
Undo restores serialized scene data, not arbitrary runtime-only world state.
Errors propagate to the editor's Console without closing the editor.
Action values
Action(T) holds before and after values. value(.undo) selects before;
value(.redo) selects after:
const change = eggy.Action(f32){ .before = 0.5, .after = 0.8 };
volume = change.value(.redo);
volume = change.value(.undo);
An action does not allocate, deep-copy pointers, or own borrowed slices. Do not store live
component pointers in history. Use changeComponent or the other host services to validate,
apply, and record a change; an action value alone does not create an undo entry.
Group changes
var group = try editor.beginAction("Arrange selection");
defer group.deinit();
try editor.changeComponent(entity, transform_change);
try editor.changeComponent(entity, mesh_change);
try group.commit();
Commit makes the children one undo step. Leaving an uncommitted group reverts its children in reverse order. Nested groups are rejected. If cleanup itself fails, it is logged; inspect the Console rather than assuming the group committed. History stores data, not callbacks into your plugin, so unloading its library cannot leave a callable stale undo function. Undo/redo of scene snapshots reconstructs entities: reacquire their IDs and components.
Add commands
pub const editor_commands = [_]eggy.plugins.EditorCommand{
.{ .key = "reset", .title = "Reset Tool State" },
};
pub fn editorCommand(self: *Plugin, command: usize, editor: *eggy.editor.Context) !void {
_ = editor;
switch (command) {
0 => self.clicks = 0,
else => unreachable,
}
}
Commands appear in View. The key is scoped by the published plugin name; titles can change. The host copies metadata and dispatches to the currently loaded plugin instance, not to a stored function pointer from its previous library.
Use host workflows for files and project changes
When a tool changes files, use createFolder, renameFile, duplicateFile, moveFile,
copyFile, importFile, or deleteFile. These services keep asset IDs in the
asset catalog and record undo history. Deletion moves files into the project's trash. Move and copy
do not depend on keyboard modifiers. Pass project-relative paths, except for the source
passed to importFile.
To write a file's contents, use writeFile(path, contents). It makes the folders the
path needs and records one undo step: undoing a new file moves it (and any folder made for
it) to the trash, and undoing a rewrite puts the old text back.
Use selection and select to read or change selection, and openScene and saveScene
to use the editor's scene workflow.
projectSettings(arena) returns eggy.project.Config, with strings owned by your arena.
projectFolder(arena) returns the open project's absolute folder, for tools that run
programs in it (the Git plugin runs git there).
setDefaultScene(path) changes the project's startup scene as an undoable setting.
Plugin preference values remain available through editor.app.preferences(Preferences).
Queue builds and project workflows
Use editor.requestBuild() or editor.requestReload() to queue the existing
Build/Reload workflow until the current hook returns. showExport, showNewProject,
and showOpenProject queue the normal editor workflow, window, or picker; they do
not complete the operation synchronously. A project transition runs after the hook
returns so it cannot unload code that is still executing.
A successful request means queued, not completed. Check the Console and Build window for errors and progress; unsaved-change prompts use the normal editor UI. Only one lifecycle request may be pending, and one cannot be queued inside an action group. Use the Console controls to filter and inspect diagnostics.
Drive the editor from a tool
A tool that works the editor on someone's behalf, such as a server an AI agent talks to, can read and change the whole scene and run the game from its hooks:
pub fn editorUpdate(self: *Plugin, editor: *eggy.editor.Context) !void {
var arena = std.heap.ArenaAllocator.init(self.gpa);
defer arena.deinit();
const now = try editor.status(arena.allocator());
if (now.play != .editing) return;
const scene = try editor.sceneText(arena.allocator());
_ = scene;
_ = try editor.addToScene(".{ .entities = .{ .{ .name = \"Rock\" } } }", null);
}
status(arena)says which scene is open and whether it is saved, whether the game is playing or paused, how the lastrequestBuildwent (with the end of its output), and what Undo and Redo would do.sceneText(arena)returns the world as scene-file ZON.replaceScene(zon)replaces every entity with that ZON, andaddToScene(zon, parent)adds its entities, under the entity at placeparentinsceneText's.entitieswhen one is given. Each is one undo step, works only while editing, and returns how many components or fields it couldn't put in; the Console says why.componentTypes(arena)lists every component a scene can hold, with each field's default value as ZON, so a tool can write components it wasn't compiled with.consoleText(arena, max_messages)returns the Console's latest messages.undoandredoqueue Edit > Undo and Redo for after the hook.play(seconds),pause,resumePlay(seconds)andstopPlayqueue play controls. Editor hooks don't run while the game runs, soplayandresumePlaypause by themselves afterseconds(0 runs until stopped), and the tool picks up again once paused.
Play, Stop, Undo of a scene step and a library reload start plugins over, so keep
nothing from app.world past the hook. A build a tool asked for with requestBuild
closes its Build window by itself when it ends; read how it went from status. When the
build changed the editor itself, the rebuilt editor opens in its place, and the tool starts
over there.
Check the edit and its undo
In editing mode, select an entity with a Transform, press Raise selection, and check that its Y position increases by one. Undo and redo the edit. A committed action group should appear as one undo step. Reacquire entity IDs and components after either operation. Inspect the Console if a change, group cleanup, or queued request fails.
Navigate the test scene
A mouse wheel zooms; right-drag looks around and flies with WASD, Alt+left-drag orbits, and middle-drag pans. On a touchpad, two-finger scrolling orbits the focus point on both axes; hold Shift to pan or Ctrl to zoom. The Scene panel distinguishes mouse and touchpad input on each scroll, so both can be used together. Native pinch gestures zoom where the desktop exposes them through SDL. Gestures start only over the Scene, outside its floating toolbar and other panels. SDL has no native twist-rotation event; use horizontal two-finger scrolling to rotate around the scene.
Enter Play to check mode restrictions. The Scene toolbar, gizmo, and orientation widget are hidden, and an accent border marks the game viewport as playing.
Migrating older hooks
See Migrating existing code for older signatures, plugin hooks for current ones, and ownership and reload for library boundaries. Project configuration covers settings and preferences. The API reference lists editor service contracts.