SDK 參考
以下內容皆來自套件的主要入口。呼叫主機方法會回傳 promise。每次主機呼叫都會先在你的框架中檢查功能權限與輸入,並在傳送任何資料前以 FORBIDDEN 或 INVALID_INPUT 失敗。本機 getter 和訂閱則是同步的。
連線
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>從網址片段讀取主機來源網域與執行個體 ID,依據 allowedHostOrigins 檢查來源網域,並完成交握。成功時會解析為客戶端;失敗時則會以 NOT_EMBEDDED、HOST_DENIED、HANDSHAKE_REJECTED、INCOMPATIBLE_API 或 HANDSHAKE_TIMEOUT 拒絕(逾時由 timeoutMs 決定,預設為 10 秒)。
情境
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 說明儲存資料的位置:資料保存在覺城伺服器時為 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): () => void自 SDK 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。每筆儲存資料最多可有 128 KiB 的純 JSON。只有當 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}>自 SDK 0.9 與主機 API 1.6 起提供。已核准的來源會收到選用的 context.progression 與 progression.changed,其內容固定為 {enabled: boolean, importExisting: boolean}。若不支援,本機 getter 會將兩者都回傳為 false。訂閱會立即收到一份副本,之後在有效變更時再收到更新,並回傳取消訂閱函式。匯入選擇只屬於所選遊戲。只有使用 postgres 或同等的 development 儲存方式時,才會提供此功能和內容。
請等主要存檔成功後,再回報其版本。回報只包含固定鍵、階級 1 或 2、規則版本、一個已儲存的版本,以及必填的要求 ID;不得包含身分、文字、圖片、位置或其他屬性。伺服器會依據核准規則檢查自己的精簡存檔投影。第 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.停止記錄時,請清除記憶體中的候選資料。不要建立外掛程式自有佇列,不要輪詢伺服器,也不要延遲存檔或離開。有限次重試由主機負責。結果不確定時,保留完全相同的回報與要求 ID。七天內,相符的收據會傳回原結果,不會占用新回報額度;內容不同則會回傳 IDEMPOTENCY_CONFLICT。遇到 STALE_EVIDENCE 時,讀取較新的存檔、重新評估,再使用新的 ID。缺少權限、記錄已停用、規則未知或工作階段過期,都會終止目前的回報。
這些裝飾是私人外觀選擇。已確認的快照由用戶端回報,明確匯入的歷史資料則會標示為已匯入。兩者都不能證明玩家確實手動遊玩或呼吸,也不得用於公開排名、交易或付款。外掛程式不會得知擁有者身分、城市配置、宿主世代或其他遊戲的進度。停止或清除城市記錄不會影響原本的遊戲存檔。
載入與工作階段
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 項數值指標。片刻圖片是 PNG 資料網址,最多 350,000 個字元;平台可能會先詢問訪客是否要留存。徽章申請會附上你的佐證,由伺服器判定;裝置模式下的結果一律不會經過驗證。
計數器、事件、分享與離開
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 起提供。最多六個操作:平台的分享操作、標準意圖(sound、restart、settings、help、undo、redo),或自訂操作。自訂標題最多 40 個字元,並可使用由最多四個 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 會通知你通道已永久關閉。請勿在 dispose 處理函式中呼叫 disconnect(),否則主機會等候一秒直到逾時。
使用限制
| 項目 | 限制 |
|---|---|
| 單則訊息 | 512 KiB |
| 儲存資料 | 128 KiB |
| 里程碑回報 | 1 KiB;每位擁有者/遊戲每 10 秒可回報 2 筆新資料,每個 UTC 日最多 40 筆;每位擁有者每個 UTC 日最多 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 | 記錄已停用:請清除候選資料。所參照的存檔尚未送達:請等待確認。已有較新的快照:請讀取、重新評估,並使用新的回報 ID。 |
INVALID_RESPONSE | 宿主未確認里程碑結果。請保留遊戲存檔,且不要宣稱裝飾已記錄。 |