Skip to main content

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 studioPluginMain throws 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.

FunctionUse
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.

  • supports decides whether your editor can open a document. Studio calls it often, so keep it quick and free of side effects.
  • create builds 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 matchResult
NoneSource Editor.
OneThat editor opens.
SeveralA 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.

CallCapabilityResult
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.transactionsApplies several edits as one Undo step named label. Returns the new revision.
studioSaveDocument(uri, revision)documents.saveSaves the document as File > Save would. Returns StudioDocumentSaveResult.
studioRevealSource(StudioSourceLocation { uri, line, column })source.revealOpens the location in Source Editor.
studioSubscribeDocument(uri), studioSubscribeDocumentLifecycle(uri)documents.changesTells you when the document changes.
studioProjectImage(uri)assets.projectReturns 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 start order, 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.

SituationResult
No unsaved changessaved: true; nothing is written.
Unsaved changes, file unchanged on diskSaved; saved: true.
File changed on disksaved: false, conflicted: true. The user resolves the conflict in Studio; plugins cannot overwrite.
Other failuresaved: false with error.
Outdated revision, closed or read-only documentError -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.
  • studioSubscribeDocument ends when the document closes (closed: true). studioSubscribeDocumentLifecycle keeps 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 setImage and setSvg are looked up next to your plugin's entry source first, then in the files that ship with Studio (for example images/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:

CodeMeaning
-920Studio is not ready, or the request is not allowed right now.
-921Document request refused: outdated revision, read-only, conflicted, closed or missing document.
-930The project was closed or the plugin reloaded while the request was waiting.
-931The plugin did not activate within 10 seconds.
-932A 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.