Studio plugins
Experimental. The plugin API (cturtle/studio, version 1) may change in
incompatible ways.
A plugin adds your own tools and editors to Studio for your game, written in BT. For example, a ship editor that opens your game's ship files with a custom form and preview, or a panel that runs a test level. Plugins live in your project; Studio loads them whenever it opens the project. Plugin views dock, move and hide like built-in views, and plugin edits go through Studio's normal Undo, Save and conflict handling.
A complete example is the ship editor in
games/dont-pee-in-your-spaceship/studio/ship/.
Files
my-game/
game.json
studio.json lists your plugins
studio/tools/plugin.json plugin descriptor
studio/tools/main.bt plugin entry source
studio.json:
{ "plugins": ["studio/tools/plugin.json"] }
studio/tools/plugin.json:
{
"id": "mygame.tools",
"name": "My Game tools",
"version": "0.1.0",
"studio_api": 1,
"source": "main.bt",
"requires": ["ui.embedded", "editors.documents", "documents.transactions"]
}
Every field, and the list of capability names for requires, is in
Studio plugins file format. Declare each
capability your plugin uses; Studio rejects unknown names.
Packaged games do not include studio.json or plugin sources unless you list
them in package.copy.
Entry point
Studio calls studioPluginMain() in your entry source. Register your tools
and editors there, then call studio_plugin_activate():
include "cturtle/ui";
include "cturtle/ui/btlib";
include "cturtle/studio";
fn studioPluginMain() -> void {
studioRegisterDocumentEditor(StudioDocumentEditorContribution {
id: "actor",
title: "Actor Editor",
supports: actorEditorSupports,
create: actorEditorCreate,
})?;
studio_plugin_activate()?;
}
- Register at least one tool or editor before activating. Nothing can be registered after activation.
- Activate within 10 seconds, or the plugin fails to load. Compiling your source does not count toward this limit.
- If
studioPluginMainthrows or returns without activating, the plugin does not load. Errors appear in Output under Plugins. Other plugins are not affected.
A plugin can use the UI library, events, the standard library, YAML and
JSON, and cturtle/studio. It cannot use game simulation or scene functions.
File reads and writes are relative to the project folder. Includes resolve
through your project's modules, plus the cturtle module that ships with
Studio.
Tools
A tool is a view you build with the UI library. Pass a screen created from
your plugin's own UI registry, typically
uiLibCreate(uiRegistry(), width, height, themeDefault()).screen. Each screen
can back only one tool or editor.
| Function | Use |
|---|---|
studioRegisterTool(StudioToolContribution { id, title, defaultRegion, surface }) | A tool built immediately and shown when the plugin loads. |
studioRegisterLazyTool(StudioLazyToolContribution { id, title, defaultRegion, create }) | A tool that appears in the View menu but is only built, by calling create(StudioViewContext) -> UiSurface, the first time the user opens it. |
defaultRegion is studioRegionLeft(), studioRegionDocument(),
studioRegionRight() or studioRegionBottom().
IDs are 1–128 characters from letters, digits, ., _ and -, unique
within your plugin. Titles cannot be empty.
Document editors
A document editor opens project files in your own UI:
fn actorEditorSupports(StudioViewContext context) -> bool {
return context.assetKind == "actors";
}
fn actorEditorCreate(StudioViewContext context) -> UiSurface {
StudioDocumentSnapshot doc = (join studio_document_read(context.uri))?;
UiLib lib = uiLibCreate(uiRegistry(), 960, 720, themeDefault());
// ...build the editor from doc.source...
return lib.screen;
}
StudioViewContext has the document's project-relative path (uri) and, if
the file is a registered asset, its asset registry kind (assetKind, for
example "actors"); otherwise assetKind is empty.
supportsdecides whether your editor can open a document. Studio calls it often, so keep it quick and free of side effects.createbuilds the editor for one document. Each document gets its own editor view, titled editor title · file name; reopening the same document reuses it.
When the user opens a file from Explorer:
| Editors that match | Result |
|---|---|
| None | Source Editor. |
| One | That editor opens. |
| Several | A menu offers Source Editor and each matching editor. |
Studio remembers the user's choice per project and asset kind (or file
extension) until Studio exits. PNG images and .effect files always use
their built-in editors.
Users can switch editors with Document: Open With, Document: Open in Source Editor, and one Document: Open With · editor title command per plugin editor.
Reading and editing documents
Documents are identified by their project-relative path, for example
data/actors/SilverArrow.yaml. Every read returns a revision number; pass it
back when you edit or save, so Studio can refuse an edit based on outdated
text.
| Call | Capability | Result |
|---|---|---|
join studio_document_read(uri) | — | Opens the document if needed. Returns StudioDocumentSnapshot { uri, revision, source }. |
join studio_document_replace(uri, revision, source) | — | Replaces the whole text as one Undo step ("Plugin edit"). Returns the new revision. |
studioApplyDocumentEdits(StudioDocumentTransaction { uri, revision, label, edits }) | documents.transactions | Applies several edits as one Undo step named label. Returns the new revision. |
studioSaveDocument(uri, revision) | documents.save | Saves the document as File > Save would. Returns StudioDocumentSaveResult. |
studioRevealSource(StudioSourceLocation { uri, line, column }) | source.reveal | Opens the location in Source Editor. |
studioSubscribeDocument(uri), studioSubscribeDocumentLifecycle(uri) | documents.changes | Tells you when the document changes. |
studioProjectImage(uri) | assets.project | Returns an image key for a project image file, for use with setImage. Do not save the key. |
Edits mark the document unsaved, appear in every editor showing it, and are
covered by crash recovery. Saving is up to the user unless you call
studioSaveDocument.
Edits
Each StudioTextEdit { start, length, replacement } replaces length bytes
at byte offset start in the text of the revision you name (UTF-8, with the
file's original line endings).
- List edits in increasing
startorder, without overlaps. - Do not split a UTF-8 character or a CRLF line ending.
- If any edit is invalid, nothing changes.
The whole group is rejected with error -921 if the document is read-only, conflicted, closed, or has changed since your revision. Read it again and retry.
Saving
StudioDocumentSaveResult has uri, revision, saved, conflicted and
error.
| Situation | Result |
|---|---|
| No unsaved changes | saved: true; nothing is written. |
| Unsaved changes, file unchanged on disk | Saved; saved: true. |
| File changed on disk | saved: false, conflicted: true. The user resolves the conflict in Studio; plugins cannot overwrite. |
| Other failure | saved: false with error. |
| Outdated revision, closed or read-only document | Error -921. |
Revealing source
line and column start at 1; column counts bytes. Out-of-range values
are clamped to the nearest valid position.
Watching for changes
A subscription has initial (the current snapshot), next() and close().
next() waits for the next change and returns
StudioDocumentChange { uri, revision, closed }; read the document again to
get the new text.
- Rapid changes are combined: you get the newest one.
- The first change may already be in
initial; compare revisions. studioSubscribeDocumentends when the document closes (closed: true).studioSubscribeDocumentLifecyclekeeps going across close and reopen.- Call
close()when your editor no longer needs updates.
A typical editor reads subscription.initial, then loops on next() in a
branch to refresh its UI.
Running the game
studioRunGame(manifestUri) (games.run) runs a project manifest, for
example a test level's game.json, in Studio's Play session, using the
current Release/Debug setting. It saves all documents first and fails while
Play is loading or a build is running. Stop, Return to Editor and debugging
work as for Play.
Input and UI behavior
- Your view receives mouse, keyboard and paste input while it has focus. Select All, Copy, Cut and Paste go to your view; Save, Undo and Redo stay Studio document commands.
- Images and SVGs named in
setImageandsetSvgare looked up next to your plugin's entry source first, then in the files that ship with Studio (for exampleimages/icons/lucide/...icons). Paths must be relative and stay inside those folders. - Popups are clipped to your view.
Reloading
Plugins: Reload (Ctrl+Alt+R) restarts all of the project's plugins from source. Open documents, unsaved text and Undo history are kept. Your views' own state (scroll positions, selections, form contents) is not.
Errors
Problems with studio.json or descriptors are listed in the
file format page. Errors your plugin
code can receive:
| Code | Meaning |
|---|---|
| -920 | Studio is not ready, or the request is not allowed right now. |
| -921 | Document request refused: outdated revision, read-only, conflicted, closed or missing document. |
| -930 | The project was closed or the plugin reloaded while the request was waiting. |
| -931 | The plugin did not activate within 10 seconds. |
| -932 | A create or supports callback failed; the message is the callback's error. |
If a create or supports callback fails or takes longer than 10 seconds,
Output shows a message such as Could not open <editor> · <reason> or
Plugin editor request timed out.
Limits
- Plugins run inside Studio with Studio's file access. Only open projects you trust.
- Plugins load from source only; compiled plugins are Not implemented.
- Plugin views remember nothing across reload or restart, and their dock position is not restored.
- Editors for closed documents stay in memory until the plugin reloads or the project closes.
- Not implemented: Inspector sections, popups outside the view, touch input, and preview viewports owned by plugins.