SDK-Referenz
Alles Folgende stammt aus dem Haupteinstieg des Pakets. Aufrufe von Host-Methoden geben Promises zurück. Jeder Host-Aufruf prüft zuerst die Fähigkeit und Eingabe in deinem Frame und schlägt mit FORBIDDEN oder INVALID_INPUT fehl, bevor etwas gesendet wird. Lokale Getter und Abonnements sind synchron.
Verbinden
import {connect, applyTheme, SDKError} from '@zenclub/sdk'
connect(options: {
pluginId: string
version: string // the manifest version
allowedHostOrigins: string[] // exact origins, such as 'https://zenclub.app'
hostApi?: {major: number, minMinor: number} // default {major: 1, minMinor: 0}
timeoutMs?: number // default 10000
}): Promise<ZenClubClient>Liest den Origin des Hosts und die Instanz-ID aus dem URL-Hash, prüft den Origin anhand von allowedHostOrigins und schließt den Handshake ab. Die Methode liefert einen Client oder wird mit NOT_EMBEDDED, HOST_DENIED, HANDSHAKE_REJECTED, INCOMPATIBLE_API oder HANDSHAKE_TIMEOUT abgelehnt (nach timeoutMs, standardmäßig 10 Sekunden).
Kontext
sdk.context: {
pluginId: string, instanceId: string, sessionId: string
protocol: 'zenclub.rpc/1'
hostApi: {major: 1, minor: 6} // undeclared legacy manifests receive 1.4 or 1.5
permissions: string[] // the granted capabilities
theme: ThemeSnapshot
sound?: {enabled: boolean} // only for 1.5 manifests, once a preference was chosen
progression?: {enabled: boolean, importExisting: boolean} // approved city source only
storageMode: 'postgres' | 'device' | 'memory' | 'development'
user?: {displayName: string} | null
locale?: string // a BCP 47 tag, such as 'ja-JP' or 'zh-TW'
timeZone?: string // such as 'Asia/Tokyo'
entitlements?: {tier: 'free' | 'basic' | 'pro'}
campaign?: {source?: string, medium?: string, campaign?: string} | null
}
sdk.supports(capability: string): booleansdk.context beschreibt diese Sitzung. storageMode zeigt, wo Speicherstände abgelegt werden: postgres, wenn sie auf den ZenClub-Servern liegen, und device, wenn sie in diesem Browser verschlüsselt bleiben; Test-Hosts verwenden memory oder development. Optionale Felder können auf älteren Hosts fehlen. Lies sie daher mit einem Ersatzwert aus.
Design
sdk.theme.current(): ThemeSnapshot
sdk.theme.subscribe(listener: (theme: ThemeSnapshot) => void): () => void
applyTheme(theme: ThemeSnapshot, root?: HTMLElement): void
ThemeSnapshot: {
themeId: string
mode: 'light' | 'dark'
modePreference?: 'light' | 'dark' | 'system'
reducedMotion: boolean
tokens: {background, surface, ink, muted, accent, accentInk, border, soft,
radius, font, step1 … step12, onStepLow, onStepHigh}
}subscribe wird sofort und bei jeder Änderung erneut aufgerufen. applyTheme schreibt die 24 Werte als --zc-*-CSS-Variablen und setzt data-mode, data-motion sowie color-scheme auf deinem Wurzelelement. Berücksichtige reducedMotion.
Klangpräferenz
// Require hostApi: {major: 1, minMinor: 5} in your manifest and connect().
sdk.sound.current(): boolean | undefined
sdk.sound.subscribe(listener: (enabled: boolean) => void): () => voidSeit SDK 0.8 und Host-API 1.5. Nur Plugins, die 1.5 oder höher anfordern, erhalten das optionale context.sound und Ereignisse vom Typ sound.changed. Beide verwenden ausschließlich {enabled: boolean}. Das Kontextfeld fehlt, wenn keine Auswahl getroffen wurde. current() gibt undefined zurück, wenn der Host keine Auswahl bereitgestellt hat: Behalte dann deinen eigenen Standard bei. subscribe erhält sofort einen booleschen Wert, wenn eine Auswahl bekannt ist, und danach bei Änderungen der Präferenz; die Methode gibt eine Funktion zum Abmelden zurück.
Die Präferenz ist ein Standard, keine erzwungene Stummschaltung. Wenn sie aktiviert ist, schalte deinen Tonregler an und starte die Wiedergabe erst nach einer Geste im Spiel. Wenn sie deaktiviert ist, bleibt der Ton aus, bis die Besuchenden ihn im Spiel einschalten. Lass deinen eigenen Tonregler verfügbar und aktualisiere ihn, wenn sich die Plattformpräferenz ändert. Beim Pausieren oder Ausblenden des Spiels muss der Ton unabhängig von der Präferenz stummgeschaltet werden.
let soundOn = sdk.sound.current() ?? false
let paused = false, hidden = false
function applySound() {
setSoundSwitch(soundOn) // your own control, including a header action if used
if (!soundOn || paused || hidden) stopAudio()
}
applySound()
const unsubscribe = sdk.sound.subscribe(enabled => {
soundOn = enabled
applySound() // a host event is not an audio-unlocking gesture
})
sdk.onLifecycle(command => {
if (command.type === 'pause') paused = true
if (command.type === 'resume') paused = false
if (command.type === 'visibility') hidden = !command.visible
if (command.type === 'dispose') { paused = true; unsubscribe(); stopAudio(); return }
applySound()
})
gameSurface.addEventListener('click', () => {
if (soundOn && !paused && !hidden) startAudioFromGesture()
})
soundButton.addEventListener('click', () => {
soundOn = !soundOn
applySound()
if (soundOn && !paused && !hidden) startAudioFromGesture()
})
sdk.onDisconnect(() => { paused = true; unsubscribe(); stopAudio() })Speicher
sdk.storage.read(slot = 'primary'): Promise<SaveEnvelope | null>
sdk.storage.write(slot: string, data: Json, options: {
expectedRevision: number // 0 for a new slot
schemaVersion: string // your save format, such as 'garden-v2'
requestId?: string // the same id for every retry of one change
}): Promise<SaveEnvelope>
sdk.storage.createAutosave(save: () => Promise<unknown>, options?: {
intervalMs?: number // 10000 to 300000, default 10000
onError?: (error: SDKError) => void
}): {schedule(): void, flush(): Promise<void>, dispose(): void}
sdk.storage.flush(): Promise<void>
SaveEnvelope: {slot: string, schemaVersion: string, revision: number, data: Json, updatedAt: string}Speicherplätze sind wie IDs benannt; primary ist der Standard. Ein Speicherstand umfasst bis zu 128 KiB einfaches JSON. Ein Schreibvorgang gelingt nur, wenn expectedRevision übereinstimmt. Andernfalls schlägt er mit CONFLICT fehl und nichts wird überschrieben. Verwende 0 für einen noch nicht vorhandenen Speicherplatz.
Private Stadtmeilensteine
sdk.progression.current(): {enabled: boolean, importExisting: boolean}
sdk.progression.subscribe(listener: (preference: {
enabled: boolean, importExisting: boolean
}) => void): () => void
sdk.milestones.report(value: {
key: 'homestead' | 'pond' | 'clouds' | 'bamboo' | 'tea' | 'stream' | 'bell' | 'lotus'
tier: 1 | 2
ruleVersion: string // exactly '<key>.1' in this release
evidence: {saveRevision: number} // confirmed primary save revision
requestId: string // one id per immutable report, 1-80 ID characters
}): Promise<{status: 'recorded' | 'already-recorded', tier: 1 | 2}>Seit SDK 0.9 und Host-API 1.6. Ein freigegebenes Plugin erhält optional context.progression und progression.changed mit genau {enabled: boolean, importExisting: boolean}. Ohne Unterstützung liefert der lokale Getter für beide Werte false. Abonnements erhalten sofort eine Kopie und danach bei gültigen Änderungen eine weitere; sie geben eine Abmeldefunktion zurück. Die Importwahl gilt nur für das ausgewählte Spiel. Fähigkeit und Kontext sind nur mit postgres oder einer gleichwertigen development-Speicherung verfügbar.
Warte, bis der primäre Spielstand erfolgreich gespeichert wurde, bevor du seine Revision meldest. Eine Meldung enthält nur den festen Schlüssel, Stufe 1 oder 2, die Regelversion, eine gespeicherte Revision und eine erforderliche Anfrage-ID; sie kann keine Identität, Texte, Bilder, Positionen oder andere Eigenschaften enthalten. Der Server prüft anhand seines eigenen minimalen Speicherauszugs, ob die freigegebene Regel erfüllt ist. Stufe 2 schließt Stufe 1 ein. Mögliche Ergebnisse sind recorded oder already-recorded, jeweils mit der aktuellen Stufe. Nach einer bestätigten Meldung ist die Dekoration verfügbar; ob du sie platzierst, entscheidest du selbst.
// Call with the envelope returned by a successful primary storage.write.
// This rule counts completed cups, never play time or sound settings.
async function reportTea(confirmedSave) {
const serverStored = ['postgres', 'development'].includes(sdk.context.storageMode)
if (!sdk.supports('milestones.report') || !serverStored || !sdk.progression.current().enabled) return
const cups = confirmedSave.data.cups
const tier = cups >= 3 ? 2 : cups >= 1 ? 1 : 0
if (!tier) return
return sdk.milestones.report({
key: 'tea', tier, ruleVersion: 'tea.1',
evidence: {saveRevision: confirmedSave.revision},
requestId: crypto.randomUUID(),
})
}
const unsubscribe = sdk.progression.subscribe(({enabled}) => {
if (!enabled) clearInMemoryCityCandidates()
})
sdk.onLifecycle(command => { if (command.type === 'dispose') unsubscribe() })
sdk.onDisconnect(unsubscribe)
// A result confirms the decoration was recorded; placement is the player's choice.
// Keep no plugin-owned outbox and never block game saves or leaving.Wenn die Aufzeichnung endet, lösche Kandidaten aus dem Arbeitsspeicher. Führe keine vom Plugin verwaltete Warteschlange und keine regelmäßigen Serverabfragen aus; verzögere niemals das Speichern oder Verlassen. Der Host verwaltet begrenzte Wiederholungen. Bei einem ungewissen Ergebnis behalte genau dieselbe Meldung und Anfrage-ID. Innerhalb von sieben Tagen liefert ein passender Beleg das ursprüngliche Ergebnis zurück, ohne das Kontingent für neue Meldungen zu verbrauchen; abweichende Inhalte führen zu IDEMPOTENCY_CONFLICT. Lies nach STALE_EVIDENCE neuere Speicherstände aus und prüfe sie mit einer neuen ID erneut. Fehlende Berechtigung, deaktivierte Aufzeichnung, unbekannte Regeln oder eine abgelaufene Sitzung beenden die aktuelle Meldung.
Diese Dekorationen sind private Entscheidungen zum Aussehen. Bestätigte Momentaufnahmen werden vom Client gemeldet; ausdrücklich importierte frühere Ergebnisse werden als importiert markiert. Weder das eine noch das andere belegt tatsächliches manuelles Spielen oder Atmen. Beides darf keine öffentlichen Ranglisten, keinen Handel und keine Zahlungen beeinflussen. Plugins erfahren nie die Identität der besitzenden Person, die Stadtgestaltung, die Host-Generation oder den Spielfortschritt anderer Spiele. Wenn du die Stadtaufzeichnung beendest oder Stadteinträge löschst, bleibt der ursprüngliche Spielstand erhalten.
Laden und Sitzungen
sdk.loading.start(): Promise<{ok: true}>
sdk.loading.ready(): Promise<{ok: true}>
sdk.loading.fail(message: string): Promise<{ok: true}> // up to 240 characters
sdk.session.report(status: {
state: 'running' | 'paused' | 'ended'
mode: 'watch' | 'manual' | 'listen'
runId?: string
}): Promise<{ok: true}>Melde ready, sobald die Besuchenden beginnen können. Melde den Sitzungsstatus bei jeder Änderung und während des Betriebs mindestens alle 12 Sekunden. Anhand dieser Meldungen erfasst die Plattform die Zeit. running vor ready zu melden, schlägt mit NOT_READY fehl.
Durchläufe, Momente und Abzeichen
sdk.runs.finish(run: {
id: string, reason: 'finished' | 'restarted', control: 'auto' | 'manual' | 'mixed'
metrics: Record<string, number>, endedAt: string
}): Promise<{recorded: true}>
sdk.moments.create(moment: {title: string, image: string, metadata?: Json})
: Promise<{id: string, stored: true}>
sdk.achievements.request(id: string, evidence: Json)
: Promise<{status: 'recorded', verified: boolean, awarded?: string[]}>runs.finish speichert zuerst ausstehende Fortschritte und erfasst dann einen Durchlauf mit bis zu 20 numerischen Messwerten. Das Bild eines Moments ist eine PNG-Daten-URL mit bis zu 350.000 Zeichen. Die Plattform kann vor dem Aufbewahren nachfragen. Ein Abzeichenantrag enthält deinen Nachweis. Der Server entscheidet; im Gerätemodus wird das Ergebnis nie geprüft.
Zähler, Ereignisse, Teilen und Verlassen
sdk.counters.add(key: string, delta: number, options?: {reason?: string, requestId?: string})
: Promise<{key: string, value: number, applied: boolean}>
sdk.events.report(name: string, props?: Record<string, string | number | boolean | null>)
: Promise<{recorded: boolean}>
sdk.shares.request(request: {kind: 'experience'} | {kind: 'badge', badgeCode: string})
: Promise<{presented: boolean}>
sdk.navigation.exit(reason?: string): Promise<{ok: true}>Ein Zähler ändert sich nur, wenn die Administration ihn eingerichtet und dich als schreibberechtigt eingetragen hat. Verwende eine requestId, damit eine Wiederholung nur einmal angewendet wird. Ereignisnamen bestehen aus Kleinbuchstaben und höchstens 64 Zeichen; dazu kommen bis zu 20 einfache Eigenschaften. navigation.exit speichert ausstehende Fortschritte und führt die Besuchenden zur Detailseite.
Immersive Anzeige
if (sdk.supports('display.immersive')) {
const {immersive, applied} = await sdk.display.immersive(true)
}Ab Host-API 1.3. Die Plattform blendet ihren Rahmen nur aus, wenn du bereit bist und die Besuchenden den immersiven Modus nicht ausgeschaltet haben. applied gibt an, ob sich etwas geändert hat. Der immersive Modus lässt sich immer verlassen.
Aktionen in der Kopfzeile
if (sdk.supports('header.setActions')) {
sdk.header.onAction(action => {
if (action.id === 'sound') toggleSound()
if (action.id === 'water') waterGarden()
})
await sdk.header.setActions([
{kind: 'standard', id: 'sound', intent: 'sound', pressed: soundOn},
{kind: 'custom', id: 'water', title: 'Water the garden',
icon: {paths: ['M12 3 C8 9 6 12 6 15 C6 18 9 21 12 21 C15 21 18 18 18 15 C18 12 16 9 12 3 Z']}},
])
}
sdk.header.clear()Ab Host-API 1.4. Bis zu sechs Aktionen: die Teilen-Aktion der Plattform, Standardaktionen (sound, restart, settings, help, undo, redo) oder eigene Aktionen mit einem Titel von höchstens 40 Zeichen und einem 24 × 24 großen Symbol aus bis zu vier SVG-Pfaden. Biete eine Alternative im Frame für Hosts ohne Kopfzeile an.
Lebenszyklus und Verbindungsabbrüche
sdk.onLifecycle(handler: (command: {
type: 'pause' | 'resume' | 'visibility' | 'dispose'
visible?: boolean
}) => void | Promise<void>): () => void
sdk.onDisconnect(handler: (error: SDKError) => void): () => void
sdk.disconnect(): voidonLifecycle empfängt pause, resume, visibility und dispose. Nach deinem Handler schreibt das SDK bei Pause, Ausblenden und Aufräumen alle automatischen Speicherstände sofort. onDisconnect meldet, dass der Kanal endgültig geschlossen wurde. Rufe disconnect() nicht innerhalb eines Dispose-Handlers auf, da der Host sonst seine Zeitüberschreitung von einer Sekunde abwartet.
Grenzwerte
| Angabe | Grenzwert |
|---|---|
| Eine Nachricht | 512 KiB |
| Speicherdaten | 128 KiB |
| Stadtmeilensteinmeldungen | 1 KiB; 2 neue Meldungen pro Person und Spiel in 10 Sekunden, 40 pro Person und Spiel pro UTC-Tag und 160 pro Person pro UTC-Tag |
| JSON-Struktur | Einfache Objekte und Arrays, endliche Zahlen, höchstens 22 Ebenen tief, 1.000 Schlüssel pro Objekt und 20.000 Elemente pro Array |
| Offene Anfragen | 32 |
| Anfragerate | Bursts mit 40, danach 20 pro Sekunde |
| Serverraten | Momente: 15 pro Minute, Abzeichen: 30 pro Minute, Zähler und Ereignisse: 120 pro Minute |
| Zeitüberschreitungen | Anfragen: 8 s, Handshake: 10 s, Bereitschaftsprüfung: 15 s, Lebenszeichen: 12 s, Aufräumen: 1 s |
Fehlercodes
Jede Ablehnung ist ein SDKError mit einem stabilen code. Meldungen sind für Protokolle gedacht, nicht für Besuchende.
| Code | Bedeutung und Vorgehen |
|---|---|
NOT_EMBEDDED · HOST_DENIED · HANDSHAKE_REJECTED · INCOMPATIBLE_API · HANDSHAKE_TIMEOUT | Außerhalb eines Frames, von einem nicht zugelassenen Host oder von einem Host ohne die benötigte Host-API geöffnet. Zeige einen ruhigen Hinweis. |
FORBIDDEN · INVALID_INPUT · NO_METHOD · TOO_LARGE · NOT_READY | Die Fähigkeit ist für diese Version nicht freigegeben oder die Eingabe verletzt eine Regel. Korrigiere den Aufruf und wiederhole ihn nicht. |
CONFLICT · IDEMPOTENCY_CONFLICT · ID_REUSE | Dein Speicherstand wurde an anderer Stelle geändert oder eine requestId mit anderen Daten wiederverwendet. Lies den Speicherstand erneut und lass die Besuchenden entscheiden. Überschreibe ihn niemals unbemerkt. |
TIMEOUT · INTERNAL · SAVE_UNCONFIRMED | Das Ergebnis ist unbekannt. Der Schreibvorgang könnte bereits erfolgt sein. Wiederhole ihn mit derselben requestId. |
BUSY · RATE_LIMIT | Zu viele Anfragen auf einmal oder in zu kurzer Zeit. Warte und versuche es dann erneut. |
GUEST_CAP · STORAGE_LIMIT | Die Besuchenden haben ihr Gastkontingent oder eine Speichergrenze erreicht. Halte den Status im Arbeitsspeicher und erkläre es freundlich. |
DISCONNECTED · DISPOSED · UNAUTHORIZED | Die Sitzung ist beendet. Beende die Arbeit und speichere nichts mehr. |
UNAVAILABLE · STORAGE_UNAVAILABLE | Dieser Host bietet die Funktion gerade nicht an. Biete eine eigene Umsetzung als Ersatz an. |
PROGRESSION_DISABLED · SAVE_NOT_READY · STALE_EVIDENCE | Die Aufzeichnung ist deaktiviert: Lösche die Kandidaten. Der angegebene Spielstand ist noch nicht angekommen: Warte auf seine Bestätigung. Es gibt eine neuere Momentaufnahme: Lies sie aus, prüfe sie erneut und verwende eine neue Melde-ID. |
INVALID_RESPONSE | Der Host hat das Ergebnis des Stadtmeilensteins nicht bestätigt. Behalte den Spielstand bei und behaupte nicht, dass die Dekoration aufgezeichnet wurde. |