← ソースコード説明書
KSC Billing API — 作者向けリファレンス
作成日: 2026-04-21 / 更新日: 2026-04-21
ストーリー作者・RPG 作者・ソシャゲ作者が KSC スクリプトから課金を操作するための API ガイド。
KSC は TS 風の言語で .ksc ファイルに書く。.ks タグ構文(@text 等)ではなく、関数・条件式・await を使える側。
TL;DR
// 所有確認(同期)
if (owned("premium_story_01")) {
jump("chapter2");
return;
}
// 購入(非同期)
const result = await purchase("premium_story_01");
if (result.status === "granted") {
jump("chapter2");
} else if (result.status === "cancelled") {
jump("paywall");
} else {
showError("購入に失敗しました: " + result.status);
}
プラットフォーム分岐
| 呼び出し | 戻り値 | 備考 |
|---|---|---|
platform() | "android" / "ios" / "switch" / "web" / "stub" | いつでも安全 |
if (platform() === "switch") {
// Switch は eShop で DLC を買う設計。アプリ内購入 UI は見せない
jump("dlc_info_screen");
} else if (platform() === "web") {
// Web preview は実購入なし
showToast("モバイルアプリでご購入ください");
} else {
jump("shop_screen");
}
所有チェック
| 呼び出し | 戻り値 | 備考 |
|---|---|---|
isEntitled(productId) | boolean | サーバー確認済みキャッシュを同期参照 |
owned(productId) | boolean | isEntitled の別名(読みやすさ用) |
// 分岐条件として
if (!owned("season_pass_01")) {
jump("season_pass_teaser");
}
// 複合条件
if (owned("premium_pack") && owned("bonus_pack")) {
unlockSecretEnding();
}
注意:isEntitled は サーバーが検証した購入履歴のキャッシュを返す。アプリ起動時の IBilling::init / 購入完了時 / リストア実行時に更新される。ネットワークを叩かないので、UI ロジックに埋め込んで OK。
購入フロー
| 呼び出し | 戻り値(await 後) |
|---|---|
await purchase(productId) | { status, productId, transactionId?, errorMessage? } |
status は以下の 5 種:
| 値 | 意味 |
|---|---|
"granted" | 成功 + サーバーレシート検証完了 |
"pending" | Ask to Buy / 親承認待ちなど。後で Entitlement 通知が来る |
"cancelled" | ユーザーが購入ダイアログをキャンセル |
"failed" | 通信・検証エラー等 |
"already_owned" | 非消費型の二重購入試行 |
非消費型(ストーリー解放など)
async function unlockChapter2() {
if (owned("premium_story_01")) {
jump("chapter2_scene_01");
return;
}
const r = await purchase("premium_story_01");
switch (r.status) {
case "granted":
case "already_owned":
jump("chapter2_scene_01");
break;
case "cancelled":
jump("paywall_menu");
break;
case "pending":
showToast("購入を承認待ちです。完了後に再度ご確認ください");
jump("paywall_menu");
break;
case "failed":
default:
showError("購入に失敗しました: " + (r.errorMessage || "不明なエラー"));
break;
}
}
消費型(ジェム・スタミナ回復)
消費型は 購入成功 → サーバーが通貨残高を加算 → クライアントが最新値を取得 の流れ。クライアント側で自前に残高を加算しないこと(サーバー権威のため)。
async function buyGemPack100() {
const r = await purchase("gem_pack_100");
if (r.status === "granted") {
// サーバーは既に Wallet.balance += 100 済み
await refreshPlayerState(); // 最新残高を取得してUIに反映
showToast("100 ジェムを獲得しました");
} else if (r.status === "cancelled") {
// 何もしない
} else {
showError("購入に失敗しました");
}
}
リストア購入
iOS は「リストア購入」ボタンが必須(審査リジェクト要因)。メニュー画面から到達できるようにする。
| 呼び出し | 戻り値(await 後) |
|---|---|
await restore() | [{ productId, grantedAt, expiresAt? }, ...] — 復元された Entitlement 配列 |
async function restorePurchases() {
showSpinner();
const entitlements = await restore();
hideSpinner();
if (entitlements.length === 0) {
showToast("復元する購入が見つかりませんでした");
} else {
showToast(entitlements.length + " 件の購入を復元しました");
}
}
商品 ID の命名規約
Play Console / App Store Connect で登録する商品 ID と一致させる:
| 種別 | 命名パターン | 例 |
|---|---|---|
| 非消費型(コンテンツ解放) | <feature>_<slug> | premium_story_01, character_pack_a |
| 消費型(通貨パック) | <currency>_pack_<amount> | gem_pack_100, gem_pack_500 |
| サブスク(Phase 2) | <feature>_sub_<interval> | vip_sub_monthly |
アンダースコアと英小数字のみ。ハイフン・大文字・日本語は両ストアで弾かれる。
商品の定義実体(PRODUCT_CATALOG)は apps/hono/src/routes/billing.ts にある。新商品追加時はここも編集する。
絶対にやってはいけないこと
- セーブデータに通貨残高を入れる — ロードで古い値が復活してチート源。
refreshPlayerState()で常にサーバーから取得。 - クライアント側で通貨を加算する —
@varAdd name="gems" value="100"は禁止。サーバーが Wallet.balance を更新するのを待つ。 - 価格を KSC 側でハードコード —
showToast("500円で解放!")は NG。ストア SDK が返すpriceLocalizedを UI 側で使う(端末 locale に依存)。 isEntitledの結果をゲーム進行フラグに焼き付ける — 返金・チャージバックで revoke される可能性があるため、分岐の度に都度読む。- Switch で purchase() を呼ぶ —
platform() === "switch"で購入 UI を隠すガードを入れる。
内部フロー(参考)
KSC: await purchase("gem_pack_100")
↓ HOST_CALL opcode
BillingHostBinding (C++)
↓ IBilling::purchase(id, callback)
platform 実装
├─ Android: BillingBridge.java + Google Play Billing v7
├─ iOS : billing_ios.mm + StoreKit 1
└─ Switch : 常に failed(eShop で買う)
↓ ストア SDK に購入 UI 委譲
↓ ユーザー決済
platform 実装 (callback)
↓ サーバー検証呼出
POST /api/billing/verify/{google,apple}
├─ Google Play Developer API v3 でレシート検証
├─ App Store Server API v2 でレシート検証
├─ StoreTransaction 作成(purchaseToken UNIQUE で冪等性)
└─ 非消費型 → Entitlement upsert、消費型 → Wallet.balance 加算
↓
platform 実装
↓ vm.resume(result object)
KSC: result が await の戻り値として利用可能に
関連ドキュメント
- 抽象層定義:
packages/core/src/interfaces/IBilling.ts - C++ 配線:
packages/native-engine/src/engine/BillingHostBinding.hpp - Android 実装:
packages/native-engine/src/platform/billing_android.cpp - iOS 実装:
packages/native-engine/src/platform/billing_ios.mm - サーバー検証:
apps/hono/src/routes/billing.ts - ネイティブ UI 設計:
packages/web/src/billing/NATIVE_UI_PLAN.md
Ad: stickyBottom (728x90)