Référence du SDK

Tout ce qui suit provient du point d’entrée principal du package. Les appels aux méthodes de l’hôte renvoient des promesses. Chaque appel à l’hôte vérifie d’abord sa capacité et ses données dans votre cadre, puis échoue avec FORBIDDEN ou INVALID_INPUT avant tout envoi. Les getters locaux et les abonnements sont synchrones.

Connexion

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>

Lit l’origine de l’hôte et l’identifiant d’instance dans le fragment de l’URL, vérifie l’origine avec allowedHostOrigins et termine la négociation. La promesse renvoie un client ou est rejetée avec NOT_EMBEDDED, HOST_DENIED, HANDSHAKE_REJECTED, INCOMPATIBLE_API ou HANDSHAKE_TIMEOUT (après timeoutMs, 10 secondes par défaut).

Contexte

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 décrit cette session. storageMode indique où vont les sauvegardes : postgres lorsqu’elles sont conservées sur les serveurs ZenClub et device lorsqu’elles restent chiffrées dans ce navigateur ; les hôtes de test utilisent memory ou development. Des champs facultatifs peuvent manquer sur les anciens hôtes : prévoyez une valeur de repli.

Thème

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 vous appelle immédiatement, puis à chaque changement. applyTheme écrit les 24 jetons sous forme de variables CSS --zc-* et définit data-mode, data-motion et color-scheme sur votre élément racine. Respectez reducedMotion.

Préférence sonore

// Require hostApi: {major: 1, minMinor: 5} in your manifest and connect().
sdk.sound.current(): boolean | undefined
sdk.sound.subscribe(listener: (enabled: boolean) => void): () => void

Depuis le SDK 0.8 et l’API de l’hôte 1.5. Seuls les plugins qui exigent la version 1.5 ou ultérieure reçoivent le champ facultatif context.sound et les événements sound.changed. Les deux n’acceptent que {enabled: boolean}. Le champ du contexte est absent si aucun choix n’a été fait. current() renvoie undefined si l’hôte n’a pas transmis de choix : conservez votre propre valeur par défaut. subscribe reçoit immédiatement un booléen lorsqu’un choix est connu, puis à chaque changement de préférence ; la fonction renvoyée permet de se désabonner.

Cette préférence définit la valeur par défaut, sans forcer la mise en sourdine. Si elle est activée, activez votre commande de son au démarrage et ne lancez l’audio qu’après un geste dans votre jeu. Si elle est désactivée, gardez le silence jusqu’à ce que le visiteur active le son dans votre jeu. Gardez votre propre commande de son et mettez-la à jour lorsque la préférence de la plateforme change. Une mise en pause ou le masquage du jeu doit toujours couper le son, quelle que soit la préférence.

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() })

Stockage

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}

Les emplacements portent des noms semblables à des identifiants ; primary est celui par défaut. Une sauvegarde contient jusqu’à 128 Kio de JSON brut. Une écriture réussit uniquement si expectedRevision correspond ; sinon, elle échoue avec CONFLICT sans rien remplacer. Utilisez 0 pour un emplacement qui n’existe pas encore.

Étapes de la ville personnelle

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}>

Depuis le SDK 0.9 et l’API de l’hôte 1.6. Une expérience approuvée reçoit le contexte facultatif context.progression et les événements progression.changed, avec exactement {enabled: boolean, importExisting: boolean}. Sans prise en charge, le getter local renvoie false pour les deux valeurs. La fonction d’abonnement transmet immédiatement une copie, puis chaque changement valide, et renvoie une fonction de désabonnement. Le choix d’importation concerne uniquement le jeu sélectionné. La capacité et le contexte sont disponibles uniquement avec le stockage postgres ou un stockage development équivalent.

Attendez que la sauvegarde principale ait réussi avant d’envoyer un rapport pour cette révision. Un rapport contient uniquement la clé fixe, le niveau 1 ou 2, la version de règle, une révision de sauvegarde et un identifiant de requête obligatoire ; il ne peut contenir ni identité, ni texte, ni illustration, ni position, ni autre propriété. Le serveur vérifie, au regard de la règle approuvée, les données minimales qu’il extrait de sa propre sauvegarde. Le niveau 2 inclut le niveau 1. Les résultats sont recorded ou already-recorded, avec le niveau actuel. Un résultat confirmé signifie que la décoration est disponible ; la personne qui joue choisit de la placer.

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

Quand l’enregistrement s’arrête, effacez les éléments candidats en mémoire. Ne gardez aucune file d’attente propre au plugin, n’interrogez aucun serveur et ne retardez jamais les sauvegardes ni la sortie. L’hôte gère les nouvelles tentatives en nombre limité. En cas de résultat incertain, conservez le rapport exact et son identifiant de requête. Dans les sept jours, un reçu correspondant renvoie le résultat d’origine sans consommer le quota d’un nouveau rapport ; un contenu différent échoue avec IDEMPOTENCY_CONFLICT. Lisez et réévaluez les sauvegardes plus récentes avec un nouvel identifiant après STALE_EVIDENCE. L’absence d’autorisation, l’enregistrement désactivé, les règles inconnues ou une session expirée interrompent le rapport en cours.

Ces décorations sont des choix d’apparence privés. Les instantanés confirmés sont signalés par le client et l’historique importé explicitement est marqué comme importé. Aucun des deux ne prouve un jeu réellement manuel ni une respiration réelle, et ils ne peuvent servir à des palmarès publics, des échanges ou des paiements. Les plugins ne découvrent ni l’identité du propriétaire, ni la disposition de la ville, ni la génération de l’hôte, ni la progression d’un autre jeu. L’arrêt ou l’effacement des données de ville laisse intacte la sauvegarde de jeu d’origine.

Chargement et sessions

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}>

Signalez ready dès que le visiteur peut commencer. Signalez l’état de la session à chaque changement et au moins toutes les 12 secondes pendant son exécution ; la plateforme mesure le temps à partir de ces rapports. Signaler running avant ready échoue avec NOT_READY.

Parties, moments et badges

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 sauvegarde d’abord la progression en attente, puis enregistre une partie avec jusqu’à 20 mesures numériques. L’image d’un moment est une URL de données PNG de 350 000 caractères maximum ; la plateforme peut demander l’accord du visiteur avant de la conserver. Une demande de badge comprend vos justificatifs. Le serveur décide et, en mode appareil, le résultat n’est jamais vérifié.

Compteurs, événements, partage et départ

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}>

Un compteur ne change que si l’opérateur l’a défini et vous a désigné comme contributeur ; conservez un requestId pour qu’une nouvelle tentative ne soit appliquée qu’une fois. Les noms d’événement sont en minuscules, limités à 64 caractères, avec jusqu’à 20 propriétés simples. navigation.exit sauvegarde la progression en attente et mène le visiteur à votre page de détails.

Affichage immersif

if (sdk.supports('display.immersive')) {
  const {immersive, applied} = await sdk.display.immersive(true)
}

Disponible depuis l’API de l’hôte 1.3. La plateforme masque son cadre uniquement lorsque vous êtes prêt et que le visiteur n’a pas désactivé le mode immersif. applied indique si un changement a eu lieu. Il est toujours possible de quitter le mode immersif.

Actions d’en-tête

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()

Disponible depuis l’API de l’hôte 1.4. Jusqu’à six actions : partage de la plateforme, intentions standard (sound, restart, settings, help, undo, redo) ou actions personnalisées avec un titre de 40 caractères maximum et une icône 24 × 24 dessinée en quatre tracés SVG au plus. Prévoyez une solution de repli dans le cadre pour les hôtes sans en-tête.

Cycle de vie et déconnexions

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 reçoit pause, resume, visibility et dispose. Après votre gestionnaire, le SDK vide chaque sauvegarde automatique lors d’une pause, d’un masquage ou de la suppression. onDisconnect vous indique que le canal est définitivement fermé. N’appelez pas disconnect() depuis un gestionnaire de suppression : l’hôte attendrait alors l’expiration de son délai d’une seconde.

Limites

ÉlémentLimite
Un message512 KiB
Données de sauvegarde128 KiB
Rapports d’étape1 Kio ; 2 nouveaux rapports par propriétaire et par jeu en 10 secondes, 40 par propriétaire et par jeu par jour UTC, et 160 par propriétaire par jour UTC
Structure JSONObjets et tableaux simples, nombres finis, profondeur maximale de 22 niveaux, 1 000 clés par objet et 20 000 éléments par tableau
Requêtes en cours32
Débit des requêtesPics de 40, puis 20 par seconde
Débits serveurMoments : 15 par minute ; badges : 30 par minute ; compteurs et événements : 120 par minute
Délais d’attenteRequêtes : 8 s, négociation : 10 s, vérification de disponibilité : 15 s, maintien : 12 s, suppression : 1 s

Codes d’erreur

Chaque rejet est une SDKError dotée d’un code stable. Les messages sont destinés aux journaux, pas aux visiteurs.

CodeSignification et action à effectuer
NOT_EMBEDDED · HOST_DENIED · HANDSHAKE_REJECTED · INCOMPATIBLE_API · HANDSHAKE_TIMEOUTOuverture hors d’un cadre, depuis un hôte non autorisé ou un hôte qui ne propose pas l’API demandée. Affichez un avis calme.
FORBIDDEN · INVALID_INPUT · NO_METHOD · TOO_LARGE · NOT_READYLa capacité n’est pas accordée pour cette version ou les données enfreignent une règle. Corrigez l’appel sans le réessayer.
CONFLICT · IDEMPOTENCY_CONFLICT · ID_REUSEVotre sauvegarde a été modifiée ailleurs, ou un requestId a été réutilisé avec d’autres données. Relisez la sauvegarde et laissez le visiteur décider. Ne la remplacez jamais sans le signaler.
TIMEOUT · INTERNAL · SAVE_UNCONFIRMEDLe résultat est inconnu. L’écriture a peut-être déjà eu lieu. Réessayez avec le même requestId.
BUSY · RATE_LIMITTrop de requêtes simultanées ou trop rapides. Attendez, puis réessayez.
GUEST_CAP · STORAGE_LIMITLe visiteur a atteint une limite d’invité ou de stockage. Gardez l’état en mémoire et informez-le avec douceur.
DISCONNECTED · DISPOSED · UNAUTHORIZEDLa session est terminée. Arrêtez le traitement et les sauvegardes.
UNAVAILABLE · STORAGE_UNAVAILABLECet hôte ne propose pas cette fonctionnalité pour le moment. Faites-le vous-même en solution de repli.
PROGRESSION_DISABLED · SAVE_NOT_READY · STALE_EVIDENCEEnregistrement désactivé : effacez les éléments candidats. La sauvegarde référencée n’est pas encore arrivée : attendez sa confirmation. Un instantané plus récent existe : lisez-le, réévaluez-le et utilisez un nouvel identifiant de rapport.
INVALID_RESPONSEL’hôte n’a pas confirmé le résultat de l’étape. Conservez la sauvegarde du jeu et n’indiquez pas que la décoration a été enregistrée.