关于 msn-js-sdk
msn-js-sdk 是 Microsoft Start 游戏 JavaScript SDK 的集成指南,介绍玩家认证、云存档、广告、分享、排行榜提交和应用内购买。它面向该平台的游戏开发者,并说明授权条件、账户类型限制及 API 的使用约束。
主要功能
- 01加载并轮询 SDK 全局对象
- 02检查玩家登录与授权状态
- 03读写云存档数据
- 04展示横幅、插页式和奖励广告
- 05分享内容并提交排行榜分数
- 06查询目录、购买及管理应用内商品
适用场景
- 01为登录玩家保存云端游戏数据
- 02在游戏中展示不同类型的广告
- 03提交游戏结果供平台排行榜显示
- 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 前,都要轮询该全局对象。
关键限制:仅当 userAccountType === 'personal' 时,才能使用 IAP。工作或学校账户无法购买附加内容。
msstart 未提供:邀请好友、加入社区、创建帖子、添加到主屏幕、评分、成就、远程配置、服务器时间、生命周期/暂停/音频事件——暂停和音频功能请使用标准浏览器的 visibilitychange/blur/focus 事件;非权威计时请使用 Date.now()。
集成工作流
- 加载脚本——注入
<script src='https://assets.msn.com/staticsb/statics/latest/msstart-games-sdk/msstart-v1.0.0-rc.21.min.js'>(或动态注入),并等待window.$msstart定义。 - 验证
$msstart——调用任何 API 前,msstart = window.$msstart必须为真值。 - 探测身份验证——调用
getSignedInUserAsync();如果调用成功,设置isPlayerAuthorized = true,并获取playerId。访客用户的错误可静默捕获。 - 限制功能使用——云存档和 IAP 需要
isPlayerAuthorized。IAP 还需要userAccountType === 'personal'。 - 显示首个插屏广告——初始化后至少等待 60 秒,再显示首个插屏广告。
- 验证云存档——依赖云存档前,先执行一次
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)。排行榜由宿主界面呈现;客户端没有读取排行榜的端点。
function leaderboardsSetScore(score) {
return msstart.submitGameResultsAsync(score);
}
应用内购买
四种操作都位于 $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;
}
}