msn-js-sdk

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MSN JavaScript SDK

MSN JavaScript SDK

Overview

概述

Microsoft Start games expose a single global
window.$msstart
once the official
msstart
script has loaded. Every feature is invoked as
window.$msstart.<method>Async(...)
and returns a Promise.
Key constraint: The
msstart
script loading does not guarantee
window.$msstart
is ready — poll for the global before calling any API.
Key constraint: IAP is only available when
userAccountType === 'personal'
. Work/school accounts cannot purchase add-ons.
Not exposed by msstart: invite friends, join community, create post, add to home screen, rate, achievements, remote config, server time, lifecycle/pause/audio events — use standard browser
visibilitychange
/
blur
/
focus
events for pause/audio and
Date.now()
for non-authoritative timing.
Microsoft Start 游戏会在官方
msstart
script 加载后公开一个全局
window.$msstart
。所有功能都通过
window.$msstart.<method>Async(...)
调用,并返回一个 Promise。
关键限制:加载
msstart
script 并不能保证
window.$msstart
已就绪——调用任何 API 前,应轮询检查该全局变量。
关键限制:仅当
userAccountType === 'personal'
时才能使用 IAP。工作/学校账户无法购买附加内容。
msstart 未提供:邀请好友、加入社区、创建帖子、添加到主屏幕、评分、成就、远程配置、服务器时间、生命周期/暂停/音频事件——暂停和音频应使用标准浏览器
visibilitychange
/
blur
/
focus
事件,非权威计时则使用
Date.now()
。

Integration Workflow

集成工作流

  1. Load script — inject
    <script src='https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js'>
    (or dynamically) and wait for
    window.$msstart
    to be defined.
  2. Verify
    $msstart
    —
    msstart = window.$msstart
    must be truthy before any API call.
  3. Probe auth — call
    getSignedInUserAsync()
    ; if it resolves,
    isPlayerAuthorized = true
    and
    playerId
    is set. Catch silently for guests.
  4. Gate features — cloud save and IAP require
    isPlayerAuthorized
    . IAP additionally requires
    userAccountType === 'personal'
    .
  5. Show first interstitial — wait at least 60 seconds after initialization before the first interstitial.
  6. Validate cloud save — do a test
    getDataAsync
    round-trip before relying on cloud save.
  1. 加载 script——注入
    <script src='https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js'></script>
    (或动态注入),并等待
    window.$msstart
    完成定义。
  2. 验证
    $msstart
    ——调用任何 API 前,
    msstart = window.$msstart
    必须为 truthy。
  3. 探测身份验证——调用
    getSignedInUserAsync()
    ;如果调用成功,则
    isPlayerAuthorized = true
    ,并且已设置
    playerId
    。对于访客,静默捕获错误。
  4. 按条件启用功能——云存档和 IAP 要求
    isPlayerAuthorized
    为 true。IAP 还要求
    userAccountType === 'personal'
    。
  5. 展示首个插屏广告——初始化后至少等待 60 秒,再展示首个插屏广告。
  6. 验证云存档——依赖云存档前,先执行一次
    getDataAsync
    往返测试。

Installation

安装

Load the versioned CDN script and pin the version:
html
<script src="https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js"></script>
For dynamic injection, use
document.createElement('script')
with
onload
/
onerror
callbacks, then poll
window.$msstart
in a
setTimeout
loop until defined or a timeout elapses.
加载版本固定且经过版本锁定的 CDN script:
html
<script src="https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js"></script>
如需动态注入,请使用带有
onload
/
onerror
回调的
document.createElement('script')
,然后在
setTimeout
循环中轮询
window.$msstart
,直至其完成定义或达到超时时间。

Initialization

初始化

js
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;
        });
}
js
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;
        });
}

Player Auth

玩家身份验证

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

    return msstart
        .signInAsync()
        .then((data) => updatePlayerInfo(data))
        .catch((error) => {
            updatePlayerInfo(null);
            throw error;
        });
}
The signed-in user object contains
playerId
,
playerDisplayName
, and
userAccountType
(
'personal'
for consumer accounts).
js
function authorizePlayer() {
    if (isPlayerAuthorized) return Promise.resolve();

    return msstart
        .signInAsync()
        .then((data) => updatePlayerInfo(data))
        .catch((error) => {
            updatePlayerInfo(null);
            throw error;
        });
}
已登录用户对象包含
playerId
、
playerDisplayName
和
userAccountType
(消费者账户为
'personal'
)。

Cloud Save

云存档

A per-game key/value bucket under
$msstart.cloudSave
, scoped by
gameId
. No per-key endpoint — read the whole object once, write the merged object on every save. Deletion is performed by writing
null
for the target keys. Requires an authorized player; fall back to
localStorage
for guests.
js
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 });
}
每个游戏在
$msstart.cloudSave
下都有一个按
gameId
限定的键值存储桶。没有针对单个 key 的端点——读取时一次性获取整个对象,每次保存时写入合并后的对象。删除操作通过为目标 key 写入
null
来完成。该功能要求玩家已授权;访客应回退到
localStorage
。
js
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 });
}

Advertisement

广告

Display Banners

展示横幅广告

Pass an array of placement strings (
'position:WxH'
) to
showDisplayAdsAsync
. At most 2 simultaneous placements are recommended. Hide all with
hideDisplayAdsAsync()
.
Allowed positions and sizes:
js
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]]
};
Simple usage:
js
msstart.showDisplayAdsAsync(['top:728x90']);
msstart.showDisplayAdsAsync(['bottom:320x50']);
msstart.hideDisplayAdsAsync();
To pick the largest fitting size for a given pixel rectangle, filter
MSN_SIZES_BY_POSITION[position]
to sizes where
s[0] <= width && s[1] <= height
, then select the entry with the greatest area and format as
'position:WxH'
.
向
showDisplayAdsAsync
传入一组广告位字符串(
'position:WxH'
)。建议同时展示不超过 2 个广告位。使用
hideDisplayAdsAsync()
隐藏所有横幅广告。
允许的广告位和尺寸:
js
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]]
};
简单用法:
js
msstart.showDisplayAdsAsync(['top:728x90']);
msstart.showDisplayAdsAsync(['bottom:320x50']);
msstart.hideDisplayAdsAsync();
要为给定的像素矩形选择能够容纳的最大尺寸,请筛选
MSN_SIZES_BY_POSITION[position]
中满足
s[0] <= width && s[1] <= height
的尺寸,然后选取面积最大的条目,并将其格式化为
'position:WxH'
。

Interstitial and Rewarded Ads

插屏广告和激励广告

Shared lifecycle:
loadAdsAsync(isRewarded)
→
showAdsAsync(instanceId)
→
showAdsCompletedAsync
. Pass
false
for interstitial,
true
for rewarded. For rewarded ads,
showAdsCompletedAsync
resolving means the reward is earned. Pause gameplay and audio while the ad is open.
js
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);
        });
}
State machine values — interstitial:
'loading' | 'opened' | 'closed' | 'failed'
; rewarded: adds
'rewarded'
.
两者共用相同的生命周期:
loadAdsAsync(isRewarded)
→
showAdsAsync(instanceId)
→
showAdsCompletedAsync
。插屏广告传入
false
,激励广告传入
true
。对于激励广告,
showAdsCompletedAsync
成功完成即表示已获得奖励。广告打开期间应暂停游戏和音频。
js
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'
。

Social — Share

社交——分享

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

Leaderboards

排行榜

Write-only —
submitGameResultsAsync(score)
. The host UI renders the leaderboard; there is no client-side read-back endpoint.
js
function leaderboardsSetScore(score) {
    return msstart.submitGameResultsAsync(score);
}
仅支持写入——
submitGameResultsAsync(score)
。排行榜由宿主 UI 呈现,客户端没有用于读回数据的端点。
js
function leaderboardsSetScore(score) {
    return msstart.submitGameResultsAsync(score);
}

In-App Purchases

应用内购买

All four operations are under
$msstart.iap
. Map local product ids to msstart product ids in your catalog. IAP requires
userAccountType === 'personal'
— check
isPaymentsSupported
before calling.
Important: Failure responses are returned in-band as
{ code: 'IAP_*_FAILURE', description }
resolutions, not rejections — always inspect
code
before treating a response as success.
全部四项操作都位于
$msstart.iap
下。需要在自己的商品目录中将本地商品 ID 映射到 msstart 商品 ID。IAP 要求
userAccountType === 'personal'
——调用前必须检查
isPaymentsSupported
。
重要:失败响应会通过
{ code: 'IAP_*_FAILURE', description }
的 fulfilled Promise 原样返回,而不是以 rejected Promise 的形式返回——将响应视为成功之前,始终必须检查
code
。

Get Catalog

获取商品目录

js
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
            };
        });
    });
}
js
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
            };
        });
    });
}

Purchase

购买

Host returns
{ receipt, receiptSignature }
or
{ code: 'IAP_PURCHASE_FAILURE', description }
.
js
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;
}
宿主返回
{ receipt, receiptSignature }
或
{ code: 'IAP_PURCHASE_FAILURE', description }
。
js
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;
}

Consume Purchase

消费购买

Consume by msstart
productId
(from cached purchase). Returns
{ consumptionReceipt, consumptionSignature }
or
{ code: 'IAP_CONSUME_FAILURE', description }
.
js
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;
}
按 msstart
productId
消费商品(使用缓存购买记录中的
productId
)。返回
{ consumptionReceipt, consumptionSignature }
或
{ code: 'IAP_CONSUME_FAILURE', description }
。
js
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;
}

Get Purchases

获取购买记录

IAP_GET_ALL_PURCHASES_FAILURE
should be treated as an empty list.
js
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;
    }
}
应将
IAP_GET_ALL_PURCHASES_FAILURE
视为空列表。
js
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;
    }
}