← ソースコード説明書

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)booleanisEntitled の別名(読みやすさ用)
// 分岐条件として
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 にある。新商品追加時はここも編集する。


絶対にやってはいけないこと

  1. セーブデータに通貨残高を入れる — ロードで古い値が復活してチート源。refreshPlayerState() で常にサーバーから取得。
  2. クライアント側で通貨を加算する@varAdd name="gems" value="100" は禁止。サーバーが Wallet.balance を更新するのを待つ。
  3. 価格を KSC 側でハードコードshowToast("500円で解放!") は NG。ストア SDK が返す priceLocalized を UI 側で使う(端末 locale に依存)。
  4. isEntitled の結果をゲーム進行フラグに焼き付ける — 返金・チャージバックで revoke される可能性があるため、分岐の度に都度読む。
  5. 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 の戻り値として利用可能に

関連ドキュメント

Ad: stickyBottom (728x90)
kaedevn - ノベルゲームを作れるプラットフォーム