You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
12 KiB
328 lines
12 KiB
import { sys } from 'cc';
|
|
|
|
/** 小游戏宿主类型(与 window 全局 API 对象一一对应) */
|
|
export enum MiniGameKind {
|
|
None = 'none',
|
|
WeChat = 'wx',
|
|
Douyin = 'tt',
|
|
Kuaishou = 'ks',
|
|
Bilibili = 'bl',
|
|
Oppo = 'qg',
|
|
QQ = 'qq',
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
wx?: MiniGameHostApi;
|
|
tt?: MiniGameHostApi;
|
|
ks?: MiniGameHostApi;
|
|
bl?: MiniGameHostApi;
|
|
qg?: MiniGameHostApi;
|
|
qq?: MiniGameHostApi;
|
|
}
|
|
}
|
|
|
|
/** 各平台 SDK 共有能力的最小接口(扩展字段均为可选) */
|
|
export interface MiniGameHostApi {
|
|
getSystemInfoSync?: () => SystemInfoLike;
|
|
createRewardedVideoAd?: (opts: { adUnitId: string }) => RewardedVideoAdLike;
|
|
createInterstitialAd?: (opts: { adUnitId: string }) => InterstitialAdLike;
|
|
onShow?: (cb: (res?: Record<string, unknown>) => void) => void;
|
|
onHide?: (cb: () => void) => void;
|
|
onMemoryWarning?: (cb: (res?: { level?: number }) => void) => void;
|
|
triggerGC?: () => void;
|
|
vibrateShort?: (opts?: object) => void;
|
|
vibrateLong?: (opts?: object) => void;
|
|
reportScene?: (opts: { sceneId: number; costTime?: number; success?: (res: unknown) => void; fail?: (res: unknown) => void }) => void;
|
|
login?: (opts: object) => void;
|
|
shareAppMessage?: (opts: object) => void;
|
|
onShareAppMessage?: (cb: () => object) => void;
|
|
showShareMenu?: (opts: object) => void;
|
|
getUpdateManager?: () => UpdateManagerLike;
|
|
showModal?: (opts: object) => void;
|
|
showToast?: (opts: object) => void;
|
|
restartMiniProgramSync?: () => void;
|
|
getUserInfo?: (opts: object) => void;
|
|
getLogManager?: () => { log: (msg: string) => void; debug: (msg: string) => void };
|
|
getLaunchOptionsSync?: () => Record<string, unknown>;
|
|
canIUse?: (api: string) => boolean;
|
|
checkFeedSubscribeStatus?: (opts: object) => void;
|
|
requestFeedSubscribe?: (opts: object) => void;
|
|
navigateToScene?: (opts: object) => void;
|
|
checkSliderBarIsAvailable?: (opts: object) => void;
|
|
addShortcut?: (opts: object) => void;
|
|
getPrivacySetting?: (opts: object) => void;
|
|
requirePrivacyAuthorize?: (opts: object) => void;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface UpdateManagerLike {
|
|
onUpdateReady?: (cb: (res: unknown) => void) => void;
|
|
onUpdateFailed?: (cb: (err: unknown) => void) => void;
|
|
onCheckForUpdate?: () => void;
|
|
applyUpdate?: () => void;
|
|
}
|
|
|
|
interface SystemInfoLike {
|
|
platform?: string;
|
|
system?: string;
|
|
SDKVersion?: string;
|
|
brand?: string;
|
|
model?: string;
|
|
}
|
|
|
|
interface RewardedVideoAdLike {
|
|
load?: () => Promise<void>;
|
|
show?: () => Promise<void>;
|
|
onLoad?: (cb: () => void) => void;
|
|
onError?: (cb: (err: unknown) => void) => void;
|
|
onClose?: (cb: (res?: { isEnded?: boolean }) => void) => void;
|
|
offLoad?: (cb: () => void) => void;
|
|
offError?: (cb: (err: unknown) => void) => void;
|
|
offClose?: (cb: (res?: { isEnded?: boolean }) => void) => void;
|
|
destroy?: () => void;
|
|
}
|
|
|
|
interface InterstitialAdLike {
|
|
load?: () => Promise<void>;
|
|
show?: () => Promise<void>;
|
|
onClose?: (cb: () => void) => void;
|
|
destroy?: () => void;
|
|
}
|
|
|
|
/**
|
|
* 小游戏平台识别与能力查询。
|
|
* 以 Cocos sys.platform 为准,window 全局仅作 API 绑定;避免 ks/tt/wx 优先级不一致。
|
|
*/
|
|
export class MiniGamePlatform {
|
|
private static _inited = false;
|
|
private static _kind = MiniGameKind.None;
|
|
private static _pf: MiniGameHostApi | null = null;
|
|
private static _isHarmonyOS = false;
|
|
private static _systemInfo: SystemInfoLike | null = null;
|
|
|
|
static init(): void {
|
|
if (this._inited) return;
|
|
this._inited = true;
|
|
this._kind = this._detectKind();
|
|
this._pf = this._bindPf(this._kind);
|
|
this._systemInfo = this._readSystemInfo();
|
|
this._isHarmonyOS = this._detectHarmonyOS(this._systemInfo);
|
|
console.log(`[MiniGamePlatform] kind=${this._kind} harmony=${this._isHarmonyOS}`);
|
|
}
|
|
|
|
static get kind(): MiniGameKind {
|
|
this.init();
|
|
return this._kind;
|
|
}
|
|
|
|
static get pf(): MiniGameHostApi | null {
|
|
this.init();
|
|
return this._pf;
|
|
}
|
|
|
|
static get isHarmonyOS(): boolean {
|
|
this.init();
|
|
return this._isHarmonyOS;
|
|
}
|
|
|
|
static get systemInfo(): SystemInfoLike | null {
|
|
this.init();
|
|
return this._systemInfo;
|
|
}
|
|
|
|
/** 抖音 / 快手 / B 站:全屏激励视频,平台挂起 JS,禁止 game.pause() */
|
|
static isFullScreenRewardedVideoHost(): boolean {
|
|
const k = this.kind;
|
|
return k === MiniGameKind.Douyin
|
|
|| k === MiniGameKind.Kuaishou
|
|
|| k === MiniGameKind.Bilibili;
|
|
}
|
|
|
|
/** 微信 overlay 激励视频:需 game.pause() 冻结主循环 */
|
|
static shouldPauseEngineForVideoAd(): boolean {
|
|
return this.kind === MiniGameKind.WeChat;
|
|
}
|
|
|
|
/**
|
|
* 激励视频拉取前检查网络(微信/抖音等均提供 getNetworkType,networkType=none 表示无网)。
|
|
* 无 API 时默认 true,避免误拦。
|
|
*/
|
|
static checkNetworkForRewardedVideo(): Promise<boolean> {
|
|
if (sys.isBrowser) {
|
|
const online = typeof navigator === 'undefined' ? true : navigator.onLine !== false;
|
|
return Promise.resolve(online);
|
|
}
|
|
this.init();
|
|
const pf = this._pf as MiniGameHostApi & {
|
|
getNetworkType?: (opts: {
|
|
success?: (res: { networkType?: string }) => void;
|
|
fail?: () => void;
|
|
}) => void;
|
|
};
|
|
const getNetworkType = pf?.getNetworkType;
|
|
if (typeof getNetworkType !== 'function') {
|
|
return Promise.resolve(true);
|
|
}
|
|
return new Promise((resolve) => {
|
|
getNetworkType.call(pf, {
|
|
success: (res) => {
|
|
const type = (res?.networkType ?? '').toLowerCase();
|
|
resolve(type !== 'none');
|
|
},
|
|
fail: () => resolve(false),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 微信 / 快手:createRewardedVideoAd 为全局单例,禁止 destroy 后频繁重建。
|
|
* @see https://developers.weixin.qq.com/minigame/dev/guide/open-ability/ad/rewarded-video-ad.html
|
|
* @see https://open.kuaishou.com/miniGameDocs/gameDev/open-function/ad.html
|
|
*/
|
|
static isRewardedVideoAdSingleton(): boolean {
|
|
const k = this.kind;
|
|
return k === MiniGameKind.WeChat || k === MiniGameKind.Kuaishou;
|
|
}
|
|
|
|
/**
|
|
* B 站激励视频展示前上报 sceneId=1007(官方示例要求)。
|
|
* @see https://miniapp.bilibili.com/small-game-doc/open/ad/IncentiveVideo
|
|
*/
|
|
static reportRewardedVideoScene(): void {
|
|
if (this.kind !== MiniGameKind.Bilibili) return;
|
|
try {
|
|
this.pf?.reportScene?.({ sceneId: 1007 });
|
|
} catch (e) {
|
|
console.warn('[MiniGamePlatform] reportScene(1007) failed', e);
|
|
}
|
|
}
|
|
|
|
/** 激励视频是否正常播完(各平台 onClose 语义) */
|
|
static isRewardedVideoCompleted(res?: { isEnded?: boolean } | null): boolean {
|
|
if (res == null) {
|
|
// 微信 <2.1.0 基础库 res 为 undefined 表示完整观看;快手无此兼容
|
|
return this.kind === MiniGameKind.WeChat;
|
|
}
|
|
return !!res.isEnded;
|
|
}
|
|
|
|
/**
|
|
* 官方兼容性:优先 canIUse,其次函数是否存在。
|
|
* @see https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/basic-library/compatibility-description/
|
|
*/
|
|
static canIUse(api: string): boolean {
|
|
this.init();
|
|
const pf = this._pf;
|
|
if (!pf) return false;
|
|
try {
|
|
if (typeof pf.canIUse === 'function') {
|
|
return !!pf.canIUse(api);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return typeof (pf as Record<string, unknown>)[api] === 'function';
|
|
}
|
|
|
|
/**
|
|
* 安全调用异步开放 API(鸿蒙/低版本不支持时走 fail,不抛同步异常阻塞业务)。
|
|
* 官方:不支持时 errNo=10302,应避免直接调用。
|
|
* @see https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/tutorial/open-capabilities/general-capabilities/miniapp-harmony-adapter
|
|
*/
|
|
static invokeAsync(
|
|
api: string,
|
|
opts: Record<string, unknown> = {},
|
|
): boolean {
|
|
this.init();
|
|
const pf = this._pf;
|
|
const fail = typeof opts.fail === 'function' ? opts.fail as (err: unknown) => void : null;
|
|
if (!pf || !this.canIUse(api)) {
|
|
fail?.({
|
|
errNo: 10302,
|
|
errorCode: 88,
|
|
errMsg: `${api}:fail The feature is not available in current operating system`,
|
|
});
|
|
return false;
|
|
}
|
|
const fn = (pf as Record<string, unknown>)[api];
|
|
if (typeof fn !== 'function') {
|
|
fail?.({
|
|
errNo: 10302,
|
|
errorCode: 88,
|
|
errMsg: `${api}:fail not a function`,
|
|
});
|
|
return false;
|
|
}
|
|
try {
|
|
(fn as (o: object) => void).call(pf, opts);
|
|
return true;
|
|
} catch (e) {
|
|
fail?.(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** 鸿蒙上视频挂载/录屏分享等能力常不可用,调用前用此判断做降级 */
|
|
static isDouyinVideoOpenCapabilityAvailable(api: string): boolean {
|
|
if (this.kind !== MiniGameKind.Douyin) return this.canIUse(api);
|
|
if (this.isHarmonyOS && !this.canIUse(api)) return false;
|
|
return this.canIUse(api);
|
|
}
|
|
|
|
private static _detectKind(): MiniGameKind {
|
|
switch (sys.platform) {
|
|
case sys.Platform.WECHAT_GAME:
|
|
return MiniGameKind.WeChat;
|
|
case sys.Platform.BYTEDANCE_MINI_GAME:
|
|
return MiniGameKind.Douyin;
|
|
default:
|
|
break;
|
|
}
|
|
// 非 Cocos 枚举覆盖的宿主(快手/B 站/OPPO/QQ)按 window 全局识别
|
|
if (typeof window !== 'undefined') {
|
|
if (window.ks) return MiniGameKind.Kuaishou;
|
|
if (window.bl) return MiniGameKind.Bilibili;
|
|
if (window.tt) return MiniGameKind.Douyin;
|
|
if (window.qg) return MiniGameKind.Oppo;
|
|
if (window.qq) return MiniGameKind.QQ;
|
|
if (window.wx) return MiniGameKind.WeChat;
|
|
}
|
|
return MiniGameKind.None;
|
|
}
|
|
|
|
private static _bindPf(kind: MiniGameKind): MiniGameHostApi | null {
|
|
if (typeof window === 'undefined') return null;
|
|
switch (kind) {
|
|
case MiniGameKind.WeChat: return window.wx ?? null;
|
|
case MiniGameKind.Douyin: return window.tt ?? null;
|
|
case MiniGameKind.Kuaishou: return window.ks ?? null;
|
|
case MiniGameKind.Bilibili: return window.bl ?? null;
|
|
case MiniGameKind.Oppo: return window.qg ?? null;
|
|
case MiniGameKind.QQ: return window.qq ?? null;
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
private static _readSystemInfo(): SystemInfoLike | null {
|
|
try {
|
|
return this._pf?.getSystemInfoSync?.() ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 抖音鸿蒙识别(OpenHarmony / HarmonyOS NEXT)。
|
|
* 官方建议优先用 system.includes('openharmony')(真机+IDE 通用);
|
|
* platform==='openHarmony' 仅真机可靠。
|
|
* @see https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/tutorial/open-capabilities/general-capabilities/miniapp-harmony-adapter
|
|
*/
|
|
private static _detectHarmonyOS(info: SystemInfoLike | null): boolean {
|
|
if (!info) return false;
|
|
const system = (info.system ?? '').toLowerCase();
|
|
if (system.includes('openharmony')) return true;
|
|
return (info.platform ?? '') === 'openHarmony'
|
|
|| (info.platform ?? '').toLowerCase() === 'openharmony';
|
|
}
|
|
}
|
|
|