Zum Hauptinhalt springen

Ein Plugin schreiben

Ein Plugin ist ein Ordner auf Ihrer Platte. Sie bearbeiten ihn in Ihrer eigenen IDE, er läuft im Editor ohne Build-Schritt, und Sie veröffentlichen ihn, wenn er fertig ist.

Anfangen

Drücken Sie im Panel Plugins auf Create. Studio fragt nach einem Ordner und schreibt eine Vorlage hinein:

my-plugin/
meta.json das Manifest
main.html das Markup Ihres Panels
main.js Ihr Code — .ts, .tsx und .jsx gehen auch
plugin.d.ts generierte Typen für die ganze API
tsconfig.json damit Ihre IDE diese Typen findet
.wasignore was beim Veröffentlichen nicht hochgeladen wird

Dateien bearbeiten, Reload from disk drücken — Ihre Änderungen sind live. Kein Bundler, kein Installationsschritt.

.ts, .tsx und .jsx werden beim Lesen kompiliert

Sie können direkt TypeScript und JSX schreiben. Die Typen stammen aus dem generierten plugin.d.ts, Ihr Editor vervollständigt also die gesamte API.

Das Manifest

{
"id": "acme.shape-spawner",
"name": "Shape Spawner",
"version": "1.0.0",
"entry": "main.tsx",
"permissions": ["scene:read", "scene:write"],
"panels": [{ "id": "main", "title": "Shapes", "entry": "main.tsx" }],
"icon": "data:image/webp;base64,…"
}

Das Symbol ist eine Data-URL statt eines Dateipfads, damit ein lokaler Ordner und ein veröffentlichtes Plugin überall gleich aussehen, wo das Plugin auftaucht — Werkzeugleiste, Panel-Kopfzeile, Bibliothekskarte. Das Veröffentlichungsformular erzeugt es aus jedem Bild für Sie.

Ihr Panel

Das einfachste Panel ist eine HTML-Datei und ein Skript:

<div id="root">Loading…</div>
<script type="module">
import { signal } from '@was/signals';

const clicks = signal(0);

init(async () => {
// editor, world und meta sind hier bereit
document.getElementById('root').textContent = `${meta.name}${world.getEntities().length} entities`;
});
</script>

Oder lassen Sie HTML ganz weg und zeigen Sie mit entry auf eine .tsx-Datei — die Runtime hängt deren Default-Export ein, und Sie bauen das Panel aus der Komponentenbibliothek des Editors:

// main.tsx
import { useState } from 'react';
import { Button } from '@was/ui';

export default function Panel() {
const [clicks, setClicks] = useState(0);
return <Button onClick={() => setClicks(clicks + 1)}>{clicks}</Button>;
}
init() wartet, bis die Welt wirklich da ist

Es läuft, sobald die Verbindung zum Editor offen und die synchronisierte Kopie der Szene bereit ist — Sie müssen nie auf eines von beiden pollen.

Vier Globals stehen immer bereit: editor (die Editor-API), world (eine lebende, synchronisierte Kopie der Szene als Entities und Komponenten), meta (Ihr Manifest) und init.

Importieren dürfen Sie @was/ecs, @was/signals, @was/engine, @was/editor-api, @was/ui, @was/icons, react und react-dom/client. Alles andere scheitert laut, statt still ins Leere zu laufen.

Der Stilvorrat ist fest

Panels aus @was/ui nutzen ein vorgebautes Stylesheet, es existieren also nur die Klassen, die es mitbringt. Beliebige Werte wie text-[13px] gehören nicht dazu — nehmen Sie ein Inline-style für Größen außerhalb der Skala.

Mit der Szene arbeiten

Sie könnten Entities von Hand über world zusammensetzen, aber für Szenen und Spaces gibt es einen besseren Weg: Fragen Sie den Editor, und Sie bekommen seine eigenen Voreinstellungen, Nummerierung und Sicherungen:

await editor.scenes.list();
await editor.scenes.create({ name: 'Chapter 2', trigger: { type: 'image', imageId } });
await editor.scenes.setTrigger(sceneId, { type: 'surface', bindingType: 'wall' });
await editor.scenes.delete(sceneId);
await editor.spaces.create({ name: 'Lobby' });

Der trigger sind nur die Daten eines Ankers, geprüft vom selben Schema, das der Editor benutzt — neue Auslösertypen funktionieren also, ohne dass sich das Plugin-Protokoll ändert. Ein Bild-Auslöser errechnet seine physische Größe aus dem Bild selbst, wenn Sie keine angeben.

Die eigene Arbeit wiederfinden

Ein Plugin, das Szenen erzeugt, muss sie später wiederfinden. Dafür sind tags da — sie liegen automatisch im Namensraum Ihres Plugins, sodass zwei Plugins einander nie überschreiben, und der Editor zeigt sie weder an noch fasst er sie an:

tags.set(entity, { kind: 'scene', card: '2' });
tags.find({ kind: 'scene' });
tags.remove(entity, 'card');

Gemeinsamer Sitzungszustand

Mancher Zustand gehört zur Besprechung, nicht zum Dokument: ein laufender Timer, eine offene Abstimmung, eine erhobene Hand. Ihn rückgängig zu machen, zu veröffentlichen oder im Projekt zu speichern wäre jeweils falsch.

Der Projektraum ist ein kleiner gemeinsamer Schlüssel-Wert-Speicher, den alle sehen, die gerade im Projekt sind:

const { now, entries } = await editor.room.get();
await editor.room.set('timer', { running: true, endsAt: now + 60_000 });
editor.on('room.changed', (state) => render(state.entries.timer));
await editor.room.delete('timer');

Er überlebt ein Neuladen der Seite, läuft nach zwölf Stunden ab und fasst bis zu 64 Schlüssel zu je 8 KB.

now ist die Uhr des Servers, und genau darum geht es

Die Rechner zweier Leute können um Minuten auseinanderliegen. Speichern Sie absolute Endzeiten von der Serveruhr und lassen Sie jeden Client selbst herunterzählen — speichern Sie nie „verbleibende Sekunden", sonst hieße ein laufender Timer, jede Sekunde für alle im Projekt in den Raum zu schreiben.

Der Server merkt sich auch, wer welchen Schlüssel geschrieben hat — das macht ehrliches Abstimmen möglich: Eine Stimme zählt nur, wenn der Schlüssel vote/<userId> tatsächlich von diesem Nutzer geschrieben wurde.

Das Viewport-Overlay

Ein Panel ist privat und lässt sich schließen — untauglich für etwas, das alle sehen sollen. Ein Panel, das mit "surface": "hud" deklariert ist, wird stattdessen als kleines Overlay über der Szene gezeichnet:

editor.hud.set({ visible: true, width: 240, height: 96 });
editor.panels.open('main'); // ein Overlay kann sein eigenes Panel herbeirufen

Es startet verborgen und zeigt sich, wenn es etwas zu zeigen hat — ein unsichtbarer transparenter Rahmen über der Szene würde Klicks verschlucken. Die Größe ist auf 640×400 begrenzt.

Das Overlay bekommt keine Kopie der Szene

Es ist für alle und die ganze Zeit offen, deshalb ist es bewusst billig: Was es anzeigen muss, legt das Panel in den Raum.

Veröffentlichen

Das Veröffentlichungsformular nimmt ein Symbol, einen Namen, bis zu 12 Tags, eine Beschreibung von höchstens 500 Zeichen, bis zu 4 Screenshots und ein Kennzeichen „nur mit Abo".

Grenzen: 512 KB pro Datei, 64 Dateien, 50 Plugins pro Konto.

Erneutes Veröffentlichen aktualisiert denselben Eintrag. Sie können ein veröffentlichtes Plugin auch zum Bearbeiten herunterladen, was seine Dateien zurück in einen Ordner Ihrer Wahl schreibt.

Assets funktionieren erst nach dem Veröffentlichen

Die Dateien eines veröffentlichten Plugins werden ordentlich ausgeliefert, also löst sich ./icon.png auf. Ein lokaler Entwicklungsordner inlined nur seine .ts, .js und .json — Bilder, auf die per Pfad verwiesen wird, erscheinen erst nach dem Veröffentlichen.

Berechtigungen

Das Manifest listet, was Ihr Plugin braucht — scene:read, scene:write, resources:write, spaces:write, collaboration:read, collaboration:write, editor:panels und weitere — und der Server weist unbekannte zurück.

Deklarieren Sie nur, was Sie brauchen

Berechtigungen sind das, was Prüfer lesen und woran Leute Ihr Plugin messen. Verlangen Sie den engsten Satz, der die Arbeit erledigt.

Und wenn Sie das Plugin einer anderen Person installieren, gehen Sie davon aus, dass es das ganze Projekt erreichen kann — installieren Sie nur, wofür Sie einen Grund zum Vertrauen haben. → Vertrauen und Prüfung


Weiter: Komponenten-Erweiterungen