Protocol
@shell/editor-protocol lets a tool inspect and change a world through JSON-RPC 2.0. The editor uses it for everything it does to a scene; the same methods serve a test, a script, or a remote inspector.
import { EditorTargetPlugin, MessagePortTransport, Method, RpcPeer } from '@shell/editor-protocol';
world.addPlugin(EditorTargetPlugin); // root world; binds the `editorProtocol` resource
const [clientTransport, serverTransport] = MessagePortTransport.pair();world.get('editorProtocol').connect(serverTransport);const client = new RpcPeer(clientTransport);
const entities = await client.request(Method.ListEntities);EditorTargetPlugin adds only the protocol server, so it is safe in any world that should be inspectable. Inside the editor, use editorSession instead of a peer of your own: session.request(method, params) and session.call(method, params) address the open scene for you.
- Every method takes an optional
scene: the name of a child scene to target. Without it the world’s own entities are targeted. - Entities are addressed by
uid, components by their serial key. Only serializable components can be addressed. - A call made while a system is running takes effect after the tick, so a method never changes data a system is iterating.
Reading
Section titled “Reading”| Method | Params | Result |
|---|---|---|
world/describe | none | Versions, whether the world is in edit mode, scenes, method names |
world/listComponents | none | Each component: key, flags, required components, metadata, fields |
world/listEntities | { scene? } | Each entity: uid, name, parent, ordered children, component keys |
world/getEntity | { uid, scene? } | Component data of one entity, and the keys of its runtime components |
world/query | { with?, without?, any?, scene? } | { uids } |
world/snapshot | { roots?, scene? } | Scene data in stable form; roots limits it to those subtrees |
Changing
Section titled “Changing”Every method in this table except world/applyScene returns { result, inverse }. inverse is a list of calls that takes the change back exactly; sending it returns the calls that redo it.
const { inverse } = await client.request(Method.SetFields, { uid, component: 'position', fields: { x: 40 },});await client.batch(inverse); // x is what it was| Method | Params | Result | Inverse |
|---|---|---|---|
world/setFields | { uid, component, fields } | none | setFields with the old values |
world/insertComponent | { uid, component, fields? } | none | removeComponent, with the requirements it added |
world/removeComponent | { uid, component, also? } | none | insertComponent with the removed data |
world/spawn | { uid?, components?, parent?, before? } | { uid } | despawn |
world/despawn | { uid } (children go with it) | none | instantiate of the subtree, references restored |
world/instantiate | { data, remap, parent?, before? } | { roots, uidMap } | despawn per root |
world/reparent | { uid, parent: uid or null, before? } | none | reparent to the old parent and place |
world/applyScene | { data, scene? } | { entities } | none: it replaces the scene’s content |
removeComponentremoves only what it names, and is refused while another component on the entity requires that one.instantiatewithremap: truegives every entity a new UID and rewrites references inside the data. Withremap: falsethe recorded UIDs are used and must be free.beforeplaces an entity among its siblings. Root entities have no order in the world.client.batch(calls)runs calls in order and stops at the first failure. The error carries the results of the calls that ran, so they can be undone with their inverses.
Running
Section titled “Running”Each of these returns { running, timeScale, tick }.
| Method | Params | Effect |
|---|---|---|
runtime/state | none | Reads the state |
runtime/pause | none | Stops the ticker: no system runs |
runtime/resume | none | Starts the ticker |
runtime/step | { frames? } | Pauses, then advances frames (1 to 600) fixed timesteps |
runtime/setTimeScale | { value } | Playback speed of a running world, 0 to 100 |
Watching
Section titled “Watching”A watch returns the current state and then notifies the client that created it, at most once per tick. A batch of calls is reported once, before the batch’s answer arrives. An entity counts as changed when one of its saved components was written, or a component was added or removed; the derived state that systems rewrite every tick does not count.
| Method | Params | Result | Then notifies |
|---|---|---|---|
world/watchStructure | { scene? } | { subscription, tick, entities } | world/structureChanged |
world/watchEntity | { uid, scene? } | { subscription, tick, state } | world/entityChanged |
world/unwatch | { subscription } | { removed } | nothing |
structureChanged carries entities (every entity that is new or whose name, parent, children, or component set changed) and despawned (UIDs). entityChanged carries the entity’s whole state, or despawned: true.
Errors
Section titled “Errors”A failed call rejects with an RpcError that has a code:
ErrorCode | Meaning |
|---|---|
InvalidParams | Malformed params, an unknown field, or a value of the wrong type |
EntityNotFound | No live entity with that UID in the targeted scene |
ComponentNotRegistered | No component with that key |
ComponentNotSerializable | The component exists but cannot be addressed |
ComponentNotFound | The entity does not have the component |
ComponentAlreadyPresent | The entity already has the component |
ComponentRequired | Another component on the entity requires this one |
SceneNotFound | No child scene with that name |
InvalidHierarchy | The reparent would make an entity its own ancestor |
MethodNotFound | Unknown method |
Requests are untrusted input: params are validated before anything changes, and an unexpected failure inside a method is answered with a generic internal error.
Adding methods
Section titled “Adding methods”world.get('editorProtocol').register('example/countEntities', (params, { target }) => { const scene = target((params as { scene?: string }).scene); return { count: scene.get('entities').numOfEntities };});Throw new RpcError(ErrorCode.InvalidParams, message) for an expected failure. A method that changes the world should return { result, inverse } so it can take part in undo.