mcpField
M
msn-js-sdk

msn-js-sdk

github.com/hungify開発016 回インストール1 週間前に更新
リポジトリ

msn-js-sdk について

msn-js-sdk は、Microsoft Start ゲーム向け JavaScript SDK の導入ガイドで、プレイヤー認証、クラウドセーブ、広告、共有、ランキング送信、アプリ内購入を扱います。プラットフォーム向けゲームの開発者を対象に、API の制約やアカウント条件も説明します。

主な機能

  1. 01SDK の読み込みとグローバル変数の待機
  2. 02プレイヤーのログインと認証状態の確認
  3. 03クラウドセーブデータの読み書き
  4. 04ディスプレイ広告、インタースティシャル広告、リワード広告
  5. 05コンテンツ共有とランキングスコア送信
  6. 06アプリ内商品の一覧、購入、管理

活用シーン

  1. 01ログイン済みプレイヤーのゲームデータをクラウド保存
  2. 02ゲーム内で複数形式の広告を表示
  3. 03ホスト側ランキング向けにゲーム結果を送信
  4. 04対象アカウントのアプリ内購入を処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/hungify/playables-skills --skill msn-js-sdk

インストール後、Claude Code で/msn-js-sdkを入力して読み込まれたか確認します。

詳細

MSN JavaScript SDK

概要

Microsoft Start ゲームでは、公式の msstart スクリプトが読み込まれると、グローバルな window.$msstart が公開されます。各機能は window.$msstart.<method>Async(...) を呼び出して使用し、Promise を返します。

重要な制約:msstart スクリプトの読み込みが完了しても、window.$msstart の準備が整っているとは限りません。API を呼び出す前に、グローバル変数が利用可能になるまでポーリングしてください。

重要な制約:IAP を利用できるのは userAccountType === 'personal' の場合のみです。職場または学校のアカウントではアドオンを購入できません。

msstart では公開されていない機能:友達の招待、コミュニティへの参加、投稿の作成、ホーム画面への追加、評価、実績、リモート設定、サーバー時刻、ライフサイクル/一時停止/オーディオイベント。一時停止やオーディオには標準のブラウザーイベント visibilitychange/blur/focus を、権威のない時刻計測には Date.now() を使用してください。

統合ワークフロー

  1. スクリプトを読み込む — <script src='https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js'> を挿入する(または動的に読み込む)し、window.$msstart が定義されるまで待ちます。
  2. $msstart を確認する — API を呼び出す前に、msstart = window.$msstart が truthy であることを確認します。
  3. 認証を確認する — getSignedInUserAsync() を呼び出します。解決された場合は、isPlayerAuthorized = true となり、playerId が設定されます。ゲストの場合は、エラーを無視します。
  4. 機能を制限する — クラウドセーブと IAP には isPlayerAuthorized が必要です。IAP にはさらに userAccountType === 'personal' が必要です。
  5. 最初のインタースティシャル広告を表示する — 初期化後、少なくとも 60 秒待ってから最初のインタースティシャル広告を表示します。
  6. クラウドセーブを検証する — クラウドセーブに依存する前に、テスト用の getDataAsync 往復処理を実行します。

インストール

バージョン付き CDN スクリプトを読み込み、バージョンを固定します。

<script src="https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js"></script>

動的に挿入する場合は、document.createElement('script') と onload/onerror コールバックを使用し、その後、定義されるかタイムアウトになるまで setTimeout ループで window.$msstart をポーリングします。

初期化

const SDK_URL =
    'https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js';

const options = {
    gameId: 'YOUR_MSN_GAME_ID',
    payments: [
        // { id: 'gem_pack_small', msn: { id: 'msstore-product-id' } }
    ]
};

let msstart = null;
let isInitialized = false;
let isPlayerAuthorized = false;
let playerId = null;
let playerName = null;
let playerExtra = null;
let isPaymentsSupported = false;

function updatePlayerInfo(data) {
    if (data) {
        isPlayerAuthorized = true;
        playerId = data.playerId;
        playerName = data.playerDisplayName;
        playerExtra = data;
        isPaymentsSupported = data.userAccountType.toLowerCase() === 'personal';
    } else {
        isPlayerAuthorized = false;
        playerId = null;
        playerName = null;
        playerExtra = null;
        isPaymentsSupported = false;
    }
}

function initialize() {
    if (isInitialized) return Promise.resolve();

    return addJavaScript(SDK_URL)
        .then(() => waitFor('$msstart'))
        .then(() => {
            msstart = window.$msstart;
            return msstart
                .getSignedInUserAsync()
                .then((data) => updatePlayerInfo(data))
                .catch(() => updatePlayerInfo(null));
        })
        .then(() => {
            isInitialized = true;
        });
}

プレイヤー認証

function authorizePlayer() {
    if (isPlayerAuthorized) return Promise.resolve();

    return msstart
        .signInAsync()
        .then((data) => updatePlayerInfo(data))
        .catch((error) => {
            updatePlayerInfo(null);
            throw error;
        });
}

サインイン済みユーザーのオブジェクトには、playerId、playerDisplayName、userAccountType(一般ユーザーアカウントの場合は 'personal')が含まれます。

クラウドセーブ

$msstart.cloudSave 配下のゲームごとのキーと値のバケットで、gameId のスコープに限定されます。キー単位のエンドポイントはありません。オブジェクト全体を一度読み込み、保存するたびにマージ済みのオブジェクトを書き込みます。削除するには、対象のキーに null を書き込みます。認証済みプレイヤーが必要です。ゲストの場合は localStorage にフォールバックしてください。

let cachedData = null;

function isCloudSaveAvailable() {
    return isPlayerAuthorized;
}

function getStorageData(key) {
    if (!isPlayerAuthorized) return Promise.reject();

    const loadCache = cachedData
        ? Promise.resolve(cachedData)
        : msstart.cloudSave.getDataAsync({ gameId: options.gameId }).then((data) => {
              cachedData = data ?? {};
              return cachedData;
          });

    return loadCache.then((data) => (Array.isArray(key) ? key.map((k) => data[k]) : data[key]));
}

function setStorageData(key, value) {
    if (!isPlayerAuthorized) return Promise.reject();

    const data = cachedData ? { ...cachedData } : {};
    if (Array.isArray(key)) {
        key.forEach((k, i) => {
            data[k] = value[i];
        });
    } else {
        data[key] = value;
    }

    return msstart.cloudSave.saveDataAsync({ data, gameId: options.gameId }).then(() => {
        cachedData = data;
    });
}

function deleteStorageData(key) {
    if (!isPlayerAuthorized) return Promise.reject();

    const payload = {};
    const keys = Array.isArray(key) ? key : [key];
    keys.forEach((k) => {
        payload[k] = null;
        if (cachedData) delete cachedData[k];
    });

    return msstart.cloudSave.saveDataAsync({ data: payload, gameId: options.gameId });
}

広告

バナー広告の表示

配置文字列('position:WxH')の配列を showDisplayAdsAsync に渡します。同時に表示する配置は最大 2 つを推奨します。すべての広告を非表示にするには hideDisplayAdsAsync() を使用します。

許可されている位置とサイズ:

const MSN_SIZES_BY_POSITION = {
    top: [
        [728, 90],
        [970, 250],
        [320, 50]
    ],
    bottom: [[320, 50]],
    left: [
        [300, 250],
        [300, 600],
        [320, 50],
        [160, 600]
    ],
    right: [
        [300, 250],
        [300, 600],
        [320, 50],
        [160, 600]
    ],
    topleft: [[300, 250]],
    topright: [[300, 250]],
    bottomleft: [[300, 250]],
    bottomright: [[300, 250]]
};

簡単な使用例:

msstart.showDisplayAdsAsync(['top:728x90']);
msstart.showDisplayAdsAsync(['bottom:320x50']);
msstart.hideDisplayAdsAsync();

指定されたピクセル領域に収まる最大サイズを選ぶには、MSN_SIZES_BY_POSITION[position] を s[0] <= width && s[1] <= height を満たすサイズに絞り込み、面積が最大の項目を選び、'position:WxH' の形式にします。

インタースティシャル広告とリワード広告

共通のライフサイクル:loadAdsAsync(isRewarded) → showAdsAsync(instanceId) → showAdsCompletedAsync。インタースティシャル広告には false、リワード広告には true を渡します。リワード広告では、showAdsCompletedAsync が解決されると報酬を獲得したことになります。広告が開いている間は、ゲームプレイとオーディオを一時停止してください。

function showInterstitial({ onOpened, onClosed, onFailed } = {}) {
    return msstart
        .loadAdsAsync(false)
        .then((adInstance) => msstart.showAdsAsync(adInstance.instanceId))
        .then((adInstance) => {
            if (onOpened) onOpened();
            return adInstance.showAdsCompletedAsync;
        })
        .then(() => {
            if (onClosed) onClosed();
        })
        .catch((error) => {
            if (onFailed) onFailed(error);
        });
}

function showRewarded({ onOpened, onRewarded, onClosed, onFailed } = {}) {
    return msstart
        .loadAdsAsync(true)
        .then((adInstance) => msstart.showAdsAsync(adInstance.instanceId))
        .then((adInstance) => {
            if (onOpened) onOpened();
            return adInstance.showAdsCompletedAsync;
        })
        .then(() => {
            if (onRewarded) onRewarded();
            if (onClosed) onClosed();
        })
        .catch((error) => {
            if (onFailed) onFailed(error);
        });
}

状態マシンの値 — インタースティシャル広告:'loading' | 'opened' | 'closed' | 'failed'。リワード広告には 'rewarded' が追加されます。

ソーシャル — 共有

function share(shareOptions) {
    // shareOptions: typically { title, text, url } — see msstart docs
    return msstart.shareAsync(shareOptions);
}

リーダーボード

書き込み専用です — submitGameResultsAsync(score)。ホスト UI がリーダーボードを表示します。クライアント側で読み戻すためのエンドポイントはありません。

function leaderboardsSetScore(score) {
    return msstart.submitGameResultsAsync(score);
}

アプリ内購入

4 つの操作はすべて $msstart.iap 配下にあります。カタログでローカルの商品 ID を msstart の商品 ID に対応付けてください。IAP には userAccountType === 'personal' が必要です — 呼び出す前に isPaymentsSupported を確認してください。

重要:失敗応答は拒否ではなく、{ code: 'IAP_*_FAILURE', description } として解決されます。応答を成功として扱う前に、必ず code を確認してください。

カタログの取得
function paymentsGetCatalog() {
    if (!options.payments) return Promise.reject();

    return msstart.iap.getAllAddOnsAsync({ productId: options.gameId }).then((msnProducts) => {
        if (msnProducts.code === 'IAP_GET_ALL_ADD_ONS_FAILURE') {
            throw new Error(msnProducts.description);
        }
        return options.payments.map((product) => {
            const platformId = product.msn?.id ?? product.id;
            const p = msnProducts.find((m) => m.productId === platformId);
            return {
                id: product.id,
                title: p.title,
                description: p.description,
                publisherName: p.publisherName,
                inAppOfferToken: p.inAppOfferToken,
                isConsumable: p.isConsumable,
                price: `${p.price.listPrice} ${p.price.currencyCode}`,
                priceCurrencyCode: p.price.currencyCode,
                priceValue: p.price.listPrice
            };
        });
    });
}
購入

ホストは { receipt, receiptSignature } または { code: 'IAP_PURCHASE_FAILURE', description } を返します。

const purchases = [];

async function paymentsPurchase(id) {
    const product = options.payments.find((p) => p.id === id);
    if (!product) return Promise.reject();
    if (!isPlayerAuthorized) await authorizePlayer();

    const platformProductId = product.msn?.id ?? product.id;
    const purchase = await msstart.iap.purchaseAsync({ productId: platformProductId });

    if (purchase.code === 'IAP_PURCHASE_FAILURE') throw new Error(purchase.description);

    const merged = { id, ...purchase.receipt, receiptSignature: purchase.receiptSignature };
    purchases.push(merged);
    return merged;
}
購入の消費

キャッシュされた購入情報の msstart productId を指定して消費します。{ consumptionReceipt, consumptionSignature } または { code: 'IAP_CONSUME_FAILURE', description } を返します。

async function paymentsConsumePurchase(id) {
    const idx = purchases.findIndex((p) => p.id === id);
    if (idx < 0) return Promise.reject();
    if (!isPlayerAuthorized) await authorizePlayer();

    const response = await msstart.iap.consumeAsync({ productId: purchases[idx].productId });
    if (response.code === 'IAP_CONSUME_FAILURE') throw new Error(response.description);

    purchases.splice(idx, 1);
    const result = {
        id,
        ...response.consumptionReceipt,
        consumptionSignature: response.consumptionSignature
    };
    delete result.productId;
    return result;
}
購入情報の取得

IAP_GET_ALL_PURCHASES_FAILURE は空のリストとして扱ってください。

async function paymentsGetPurchases() {
    if (!isPlayerAuthorized) await authorizePlayer();

    try {
        const response = await msstart.iap.getAllPurchasesAsync({ productId: options.gameId });
        purchases.length = 0;
        response.receipts.forEach((purchase) => {
            const product = options.payments.find(
                (p) => (p.msn?.id ?? p.id) === purchase.productId
            );
            purchases.push({
                id: product ? product.id : purchase.productId,
                ...purchase,
                receiptSignature: response.receiptSignature
            });
        });
        return purchases.slice();
    } catch (error) {
        if (error?.code === 'IAP_GET_ALL_PURCHASES_FAILURE') return [];
        throw error;
    }
}

コメント