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): boolean

sdk.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): () => void

Seit 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(): void

onLifecycle 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

AngabeGrenzwert
Eine Nachricht512 KiB
Speicherdaten128 KiB
Stadtmeilensteinmeldungen1 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-StrukturEinfache Objekte und Arrays, endliche Zahlen, höchstens 22 Ebenen tief, 1.000 Schlüssel pro Objekt und 20.000 Elemente pro Array
Offene Anfragen32
AnfragerateBursts mit 40, danach 20 pro Sekunde
ServerratenMomente: 15 pro Minute, Abzeichen: 30 pro Minute, Zähler und Ereignisse: 120 pro Minute
ZeitüberschreitungenAnfragen: 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.

CodeBedeutung und Vorgehen
NOT_EMBEDDED · HOST_DENIED · HANDSHAKE_REJECTED · INCOMPATIBLE_API · HANDSHAKE_TIMEOUTAuß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_READYDie 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_REUSEDein 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_UNCONFIRMEDDas Ergebnis ist unbekannt. Der Schreibvorgang könnte bereits erfolgt sein. Wiederhole ihn mit derselben requestId.
BUSY · RATE_LIMITZu viele Anfragen auf einmal oder in zu kurzer Zeit. Warte und versuche es dann erneut.
GUEST_CAP · STORAGE_LIMITDie Besuchenden haben ihr Gastkontingent oder eine Speichergrenze erreicht. Halte den Status im Arbeitsspeicher und erkläre es freundlich.
DISCONNECTED · DISPOSED · UNAUTHORIZEDDie Sitzung ist beendet. Beende die Arbeit und speichere nichts mehr.
UNAVAILABLE · STORAGE_UNAVAILABLEDieser Host bietet die Funktion gerade nicht an. Biete eine eigene Umsetzung als Ersatz an.
PROGRESSION_DISABLED · SAVE_NOT_READY · STALE_EVIDENCEDie 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_RESPONSEDer Host hat das Ergebnis des Stadtmeilensteins nicht bestätigt. Behalte den Spielstand bei und behaupte nicht, dass die Dekoration aufgezeichnet wurde.