SDKリファレンス
以下はすべてパッケージのメインエントリーに基づいています。ホストメソッドの呼び出し結果は非同期です。各ホスト呼び出しでは、送信前にフレーム内で権限と入力を確認し、問題があればFORBIDDENまたはINVALID_INPUTとして失敗します。ローカルの取得関数と購読は同期処理です。
接続
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>URLハッシュからホストのオリジンとインスタンスIDを読み取り、オリジンをallowedHostOriginsと照合してハンドシェイクを完了します。クライアントを返すか、既定で10秒のtimeoutMs経過後にNOT_EMBEDDED、HOST_DENIED、HANDSHAKE_REJECTED、INCOMPATIBLE_API、HANDSHAKE_TIMEOUTのいずれかで拒否されます。
コンテキスト
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は現在のセッションを表します。storageModeは保存先を示します。ZenClubのサーバーに保存する場合はpostgres、このブラウザー内で暗号化して保持する場合はdeviceです。テストホストではmemoryまたはdevelopmentを使います。古いホストでは任意項目がない場合があるため、代替値を用意して読み取ってください。
テーマ
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は呼び出し直後と変更のたびに通知します。applyThemeは24個のトークンを--zc-* CSS変数として設定し、ルート要素にdata-mode、data-motion、color-schemeを設定します。reducedMotionを尊重してください。
音の設定
// Require hostApi: {major: 1, minMinor: 5} in your manifest and connect().
sdk.sound.current(): boolean | undefined
sdk.sound.subscribe(listener: (enabled: boolean) => void): () => voidSDK 0.8およびホストAPI 1.5から利用できます。1.5を要求するプラグインだけが、任意のcontext.soundとsound.changedイベントを受け取ります。どちらも{enabled: boolean}のみを受け付けます。選択されていない場合、コンテキストにこのフィールドはありません。ホストから選択が届いていない場合、current()はundefinedを返します。独自の初期設定を保ってください。選択がわかっている場合、subscribeはすぐに真偽値を受け取り、その後も設定が変わるたびに受け取ります。戻り値は購読解除関数です。
これは初期設定であり、強制的に消音するものではありません。有効な場合はゲーム内の音スイッチをオンにし、ゲーム内で操作があった後にだけ音声を再生してください。無効な場合は、訪問者がゲーム内で音をオンにするまで無音にしてください。ゲーム独自の音量操作を残し、プラットフォームの設定が変わったら反映してください。設定にかかわらず、ゲームを一時停止または非表示にしたときは必ず消音してください。
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() })保存
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}スロット名はIDのように指定し、既定値はprimaryです。保存できるプレーンJSONは最大128 KiBです。expectedRevisionが一致した場合だけ書き込みが成功します。一致しない場合はCONFLICTで失敗し、上書きされません。まだ存在しないスロットには0を使います。
非公開の街の節目
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}>開発キット0.9、ホスト仕様1.6から利用できます。承認済みのゲームには、任意のcontext.progressionとprogression.changedが、{enabled: boolean, importExisting: boolean}の形式で渡されます。未対応の場合、ローカルの取得関数はどちらも偽を返します。購読ではすぐに複製が渡され、その後も有効な変更ごとに通知されます。購読の戻り値は購読解除関数です。取り込みの選択は選んだゲームだけに適用されます。この機能とコンテキストは、postgresまたは同等のdevelopmentストレージでのみ利用できます。
メインのセーブデータが正常に保存され、そのリビジョンが確認されてから報告してください。報告に含められるのは、固定キー、段階1または2、ルールのバージョン、保存済みのリビジョン1つ、必須のリクエスト識別子だけです。本人情報、文章、画像、配置、その他の項目は含められません。サーバーは承認済みのルールに照らして、必要最小限のセーブデータ項目を検査します。段階2には段階1が含まれます。結果はrecordedまたはalready-recordedと現在の段階です。確認済みの結果によって飾りが利用可能になります。配置するかどうかはプレイヤーが決めます。
// 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.記録が停止したら、メモリ上の候補を消去してください。プラグイン独自のキューを持たず、サーバーにポーリングせず、保存や終了を遅らせないでください。再試行は回数を制限してホストが行います。結果が不確かな場合は、同じ報告内容とリクエスト識別子を保持してください。7日以内に内容が一致する受付記録があれば、新しい報告枠を使わずに元の結果が返ります。内容が異なるとIDEMPOTENCY_CONFLICTになります。STALE_EVIDENCEの後は、より新しいセーブデータを読み直して再評価し、新しい識別子で報告してください。権限不足、記録の無効化、未知のルール、セッションの期限切れが発生したら、その報告を中止してください。
これらの飾りは非公開の見た目の選択肢です。確認済みの状態はクライアントから報告され、明示的に取り込んだ履歴は「取り込み済み」として記録されます。どちらも実際の手動プレイや呼吸の証明ではなく、公開ランキング、取引、支払いに使ってはいけません。プラグインに所有者の身元、街の配置、ホストの世代、他のゲームの進行状況が知られることはありません。街の記録を停止または消去しても、元のゲームのセーブデータはそのまま残ります。
読み込みとセッション
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}>訪問者が始められる状態になったらreadyを報告します。セッション状態が変わるたびに、実行中は少なくとも12秒ごとに報告してください。プラットフォームはこの報告を使って時間を計測します。readyより前にrunningを報告するとNOT_READYで失敗します。
プレイ、ひととき、バッジ
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は保留中の進行状況を先に保存してから、最大20個の数値指標とともにプレイを記録します。ひとときの画像は最大350,000文字のPNGデータURLで、プラットフォームが保存前に訪問者へ確認する場合があります。バッジ申請には証拠を含めます。判定はサーバーが行い、deviceモードでは結果は検証されません。
カウンター、イベント、共有、退出
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}>カウンターが変わるのは、運営者が定義し、あなたを書き込み担当に指定した場合だけです。再試行が一度だけ適用されるようrequestIdを保持してください。イベント名は小文字で64文字以内、フラットなプロパティは最大20個です。navigation.exitは保留中の進行状況を保存し、訪問者を詳細ページに移動させます。
没入型表示
if (sdk.supports('display.immersive')) {
const {immersive, applied} = await sdk.display.immersive(true)
}ホストAPI 1.3以降で利用できます。準備が整い、訪問者が没入モードをオフにしていない場合に限り、プラットフォームはフレームを隠します。appliedで変更の有無を確認できます。没入モードから戻る操作は常に利用できます。
ヘッダーのアクション
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()ホストAPI 1.4以降で利用できます。アクションは最大6個です。プラットフォームの共有アクション、標準インテント(sound、restart、settings、help、undo、redo)、またはタイトルが40文字以内で最大4つのSVGパスからなる24 × 24アイコンを使う独自アクションを指定できます。ヘッダーのないホスト向けにフレーム内の代替操作も用意してください。
ライフサイクルと切断
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はpause、resume、visibility、disposeを受け取ります。ハンドラーの後、SDKは一時停止、非表示、破棄時にすべての自動保存をフラッシュします。onDisconnectはチャネルが完全に閉じたことを通知します。破棄ハンドラー内でdisconnect()を呼び出さないでください。ホストが1秒のタイムアウトを待つことになります。
上限
| 項目 | 上限 |
|---|---|
| 1メッセージ | 512 KiB |
| 保存データ | 128 KiB |
| 節目の報告 | 1キビバイト、所有者・ゲームごとに10秒間で新規報告2件、協定世界時の1日あたり所有者・ゲームごとに40件、所有者ごとに160件 |
| JSONの構造 | プレーンオブジェクトと配列、有限数値、最大22階層、オブジェクトあたり1,000キー、配列あたり20,000項目 |
| 同時処理中のリクエスト | 32 |
| リクエスト頻度 | 最初の40件は一括、その後は毎秒20件 |
| サーバー側の頻度 | ひとときは毎分15件、バッジは毎分30件、カウンターとイベントは毎分120件 |
| タイムアウト | リクエスト8秒、ハンドシェイク10秒、準備確認15秒、接続維持12秒、破棄1秒 |
エラーコード
拒否はすべて、安定したcodeを持つSDKErrorです。メッセージはログ用で、訪問者向けではありません。
| コード | 意味と対処 |
|---|---|
NOT_EMBEDDED · HOST_DENIED · HANDSHAKE_REJECTED · INCOMPATIBLE_API · HANDSHAKE_TIMEOUT | フレーム外、許可していないホスト、または必要なホストAPIを提供しないホストから開かれています。落ち着いた案内を表示してください。 |
FORBIDDEN · INVALID_INPUT · NO_METHOD · TOO_LARGE · NOT_READY | このバージョンでは機能が許可されていないか、入力がルールに違反しています。呼び出しを修正し、再試行はしないでください。 |
CONFLICT · IDEMPOTENCY_CONFLICT · ID_REUSE | 保存データが別の場所で変更されたか、別のデータでrequestIdが再利用されました。保存を読み直し、訪問者に選んでもらってください。知らせずに上書きしてはいけません。 |
TIMEOUT · INTERNAL · SAVE_UNCONFIRMED | 結果は不明です。書き込みがすでに完了している可能性があります。同じrequestIdで再試行してください。 |
BUSY · RATE_LIMIT | リクエストが同時に多すぎるか、頻度が高すぎます。少し待ってから再試行してください。 |
GUEST_CAP · STORAGE_LIMIT | 訪問者がゲスト利用枠または保存上限に達しました。状態をメモリに保持し、穏やかに伝えてください。 |
DISCONNECTED · DISPOSED · UNAUTHORIZED | セッションは終了しています。処理と保存を停止してください。 |
UNAVAILABLE · STORAGE_UNAVAILABLE | このホストでは現在その機能を利用できません。独自の処理に切り替えてください。 |
PROGRESSION_DISABLED · SAVE_NOT_READY · STALE_EVIDENCE | 記録は無効です。候補を消去してください。参照したセーブデータはまだ届いていません。確認されるまでお待ちください。新しいスナップショットがあります。読み直して再評価し、新しい報告識別子を使ってください。 |
INVALID_RESPONSE | ホストから節目の結果を確認できませんでした。ゲームのセーブデータは保持し、飾りが記録されたと伝えないでください。 |