msn-js-sdk
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMSN JavaScript SDK
MSN JavaScript SDK
Overview
概述
Microsoft Start games expose a single global once the official script has loaded. Every feature is invoked as and returns a Promise.
window.$msstartmsstartwindow.$msstart.<method>Async(...)Key constraint: The script loading does not guarantee is ready — poll for the global before calling any API.
msstartwindow.$msstartKey constraint: IAP is only available when . Work/school accounts cannot purchase add-ons.
userAccountType === 'personal'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 // events for pause/audio and for non-authoritative timing.
visibilitychangeblurfocusDate.now()Microsoft Start 游戏会在官方 script 加载后公开一个全局 。所有功能都通过 调用,并返回一个 Promise。
msstartwindow.$msstartwindow.$msstart.<method>Async(...)关键限制:加载 script 并不能保证 已就绪——调用任何 API 前,应轮询检查该全局变量。
msstartwindow.$msstart关键限制:仅当 时才能使用 IAP。工作/学校账户无法购买附加内容。
userAccountType === 'personal'msstart 未提供:邀请好友、加入社区、创建帖子、添加到主屏幕、评分、成就、远程配置、服务器时间、生命周期/暂停/音频事件——暂停和音频应使用标准浏览器 // 事件,非权威计时则使用 。
visibilitychangeblurfocusDate.now()Integration Workflow
集成工作流
- Load script — inject (or dynamically) and wait for
<script src='https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js'>to be defined.window.$msstart - Verify —
$msstartmust be truthy before any API call.msstart = window.$msstart - Probe auth — call ; if it resolves,
getSignedInUserAsync()andisPlayerAuthorized = trueis set. Catch silently for guests.playerId - Gate features — cloud save and IAP require . IAP additionally requires
isPlayerAuthorized.userAccountType === 'personal' - Show first interstitial — wait at least 60 seconds after initialization before the first interstitial.
- Validate cloud save — do a test round-trip before relying on cloud save.
getDataAsync
- 加载 script——注入 (或动态注入),并等待
<script src='https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js'></script>完成定义。window.$msstart - 验证 ——调用任何 API 前,
$msstart必须为 truthy。msstart = window.$msstart - 探测身份验证——调用 ;如果调用成功,则
getSignedInUserAsync(),并且已设置isPlayerAuthorized = true。对于访客,静默捕获错误。playerId - 按条件启用功能——云存档和 IAP 要求 为 true。IAP 还要求
isPlayerAuthorized。userAccountType === 'personal' - 展示首个插屏广告——初始化后至少等待 60 秒,再展示首个插屏广告。
- 验证云存档——依赖云存档前,先执行一次 往返测试。
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 with / callbacks, then poll in a loop until defined or a timeout elapses.
document.createElement('script')onloadonerrorwindow.$msstartsetTimeout加载版本固定且经过版本锁定的 CDN script:
html
<script src="https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js"></script>如需动态注入,请使用带有 / 回调的 ,然后在 循环中轮询 ,直至其完成定义或达到超时时间。
onloadonerrordocument.createElement('script')setTimeoutwindow.$msstartInitialization
初始化
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 , , and ( for consumer accounts).
playerIdplayerDisplayNameuserAccountType'personal'js
function authorizePlayer() {
if (isPlayerAuthorized) return Promise.resolve();
return msstart
.signInAsync()
.then((data) => updatePlayerInfo(data))
.catch((error) => {
updatePlayerInfo(null);
throw error;
});
}已登录用户对象包含 、 和 (消费者账户为 )。
playerIdplayerDisplayNameuserAccountType'personal'Cloud Save
云存档
A per-game key/value bucket under , scoped by . No per-key endpoint — read the whole object once, write the merged object on every save. Deletion is performed by writing for the target keys. Requires an authorized player; fall back to for guests.
$msstart.cloudSavegameIdnulllocalStoragejs
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 });
}每个游戏在 下都有一个按 限定的键值存储桶。没有针对单个 key 的端点——读取时一次性获取整个对象,每次保存时写入合并后的对象。删除操作通过为目标 key 写入 来完成。该功能要求玩家已授权;访客应回退到 。
$msstart.cloudSavegameIdnulllocalStoragejs
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 () to . At most 2 simultaneous placements are recommended. Hide all with .
'position:WxH'showDisplayAdsAsynchideDisplayAdsAsync()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 to sizes where , then select the entry with the greatest area and format as .
MSN_SIZES_BY_POSITION[position]s[0] <= width && s[1] <= height'position:WxH'向 传入一组广告位字符串()。建议同时展示不超过 2 个广告位。使用 隐藏所有横幅广告。
showDisplayAdsAsync'position:WxH'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: → → . Pass for interstitial, for rewarded. For rewarded ads, resolving means the reward is earned. Pause gameplay and audio while the ad is open.
loadAdsAsync(isRewarded)showAdsAsync(instanceId)showAdsCompletedAsyncfalsetrueshowAdsCompletedAsyncjs
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: ; rewarded: adds .
'loading' | 'opened' | 'closed' | 'failed''rewarded'两者共用相同的生命周期: → → 。插屏广告传入 ,激励广告传入 。对于激励广告, 成功完成即表示已获得奖励。广告打开期间应暂停游戏和音频。
loadAdsAsync(isRewarded)showAdsAsync(instanceId)showAdsCompletedAsyncfalsetrueshowAdsCompletedAsyncjs
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 — . The host UI renders the leaderboard; there is no client-side read-back endpoint.
submitGameResultsAsync(score)js
function leaderboardsSetScore(score) {
return msstart.submitGameResultsAsync(score);
}仅支持写入——。排行榜由宿主 UI 呈现,客户端没有用于读回数据的端点。
submitGameResultsAsync(score)js
function leaderboardsSetScore(score) {
return msstart.submitGameResultsAsync(score);
}In-App Purchases
应用内购买
All four operations are under . Map local product ids to msstart product ids in your catalog. IAP requires — check before calling.
$msstart.iapuserAccountType === 'personal'isPaymentsSupportedImportant: Failure responses are returned in-band as resolutions, not rejections — always inspect before treating a response as success.
{ code: 'IAP_*_FAILURE', description }code全部四项操作都位于 下。需要在自己的商品目录中将本地商品 ID 映射到 msstart 商品 ID。IAP 要求 ——调用前必须检查 。
$msstart.iapuserAccountType === 'personal'isPaymentsSupported重要:失败响应会通过 的 fulfilled Promise 原样返回,而不是以 rejected Promise 的形式返回——将响应视为成功之前,始终必须检查 。
{ code: 'IAP_*_FAILURE', description }codeGet 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 or .
{ 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;
}宿主返回 或 。
{ 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 (from cached purchase). Returns or .
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;
}按 msstart 消费商品(使用缓存购买记录中的 )。返回 或 。
productIdproductId{ 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_FAILUREjs
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_FAILUREjs
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;
}
}