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.
 
 
 
 
AdLike2048/assets/script/game/DeviceCapability.ts

351 lines
12 KiB

import { sys } from 'cc';
import { BYTEDANCE, WECHAT } from 'cc/env';
import type { MemoryTierId } from './memory-profile.json';
import { MiniGamePf } from '../../mx/tools/MiniGameHostInfo';
import {
douyinDeviceScoreToQuality,
memoryMbToQuality,
type MiniGameOs,
wechatBenchmarkToQuality,
wechatModelLevelToQuality,
webDeviceMemoryToQuality,
} from './device-capability-standards';
/** 设备综合性能档(画质 / 内存策略共用) */
export type DeviceQuality = 'low' | 'medium' | 'high';
export type CapabilityProbeSource =
| 'wx.getDeviceBenchmarkInfo'
| 'wx.getDeviceInfo'
| 'tt.deviceScore'
| 'legacy.getSystemInfoSync'
| 'web.deviceMemory'
| 'web.hardwareConcurrency'
| 'fallback';
/** 参与分档的运行时信号(不含机型原文,可打日志) */
export interface DeviceCapabilitySignals {
platform: string;
os: string;
miniGameOs: MiniGameOs;
probeSource: CapabilityProbeSource;
/** Chrome / 部分 WebView,单位 GB */
deviceMemoryGb?: number;
/** 逻辑核数 */
hardwareConcurrency?: number;
/** 微信 benchmarkLevel(getDeviceBenchmarkInfo 优先) */
benchmarkLevel?: number;
/** 微信 modelLevel:0 未知,1 高,2 中,3 低 */
modelLevel?: number;
/** 抖音 deviceScore.overall,0–10 */
deviceScoreOverall?: number;
/** 物理内存 MB(微信 memorySize 等) */
memorySizeMb?: number;
}
type PlatformApi = {
getDeviceBenchmarkInfo?: (opts: {
success?: (res: { benchmarkLevel?: number; modelLevel?: number }) => void;
fail?: () => void;
}) => void;
getDeviceInfo?: () => { benchmarkLevel?: number; memorySize?: string | number; platform?: string };
getSystemInfoSync?: () => Record<string, unknown>;
};
const PROBE_TIMEOUT_MS = 800;
/**
* 运行时设备能力探测(唯一分档入口)。
* 小游戏:优先官方 API(微信 getDeviceBenchmarkInfo / 抖音 deviceScore)及文档阈值;
* Web/桌面:deviceMemory + 核数;无信号时再 OS 兜底。
*/
export class DeviceCapability {
private static _resolved = false;
private static _platformProbed = false;
private static _probePromise: Promise<void> | null = null;
private static _quality: DeviceQuality = 'medium';
private static _memoryTier: MemoryTierId = 'tier0';
private static _signals: DeviceCapabilitySignals = {
platform: '',
os: '',
miniGameOs: 'unknown',
probeSource: 'fallback',
};
/** Boot 前完成:拉取微信/抖音官方性能数据后再定档 */
static ensurePlatformProbe(): Promise<void> {
if (this._platformProbed) return Promise.resolve();
if (!this._probePromise) {
this._probePromise = this._runPlatformProbe();
}
return this._probePromise;
}
static resolve(): void {
if (this._resolved) return;
this._resolved = true;
if (!this._platformProbed) {
this._signals = { ...this._signals, ...this._collectWebSignals() };
}
this._quality = this._scoreQuality(this._signals);
this._memoryTier = this._qualityToMemoryTier(this._quality);
}
static get quality(): DeviceQuality {
this.resolve();
return this._quality;
}
static get memoryTier(): MemoryTierId {
this.resolve();
return this._memoryTier;
}
static get signals(): Readonly<DeviceCapabilitySignals> {
this.resolve();
return this._signals;
}
/** 调试 / 设置面板强制档位 */
static overrideQuality(quality: DeviceQuality): void {
this._resolved = true;
this._platformProbed = true;
this._quality = quality;
this._memoryTier = this._qualityToMemoryTier(quality);
}
private static _runPlatformProbe(): Promise<void> {
const base = this._collectWebSignals();
this._signals = { ...this._signals, ...base };
const pf = gg?.sdk?.pf as PlatformApi | null | undefined;
if (pf) {
return Promise.race([
this._probeMiniGamePlatform(pf),
new Promise<void>((r) => setTimeout(r, PROBE_TIMEOUT_MS)),
]).then(() => {
this._platformProbed = true;
this._recomputeAfterProbe();
});
}
this._platformProbed = true;
this._recomputeAfterProbe();
return Promise.resolve();
}
private static _probeMiniGamePlatform(pf: PlatformApi): Promise<void> {
if (MiniGamePf.isKs()) {
this._probeLegacySystemInfo(pf);
return Promise.resolve();
}
if (WECHAT) {
return this._probeWeChat(pf);
}
if (BYTEDANCE) {
this._probeDouyin(pf);
return Promise.resolve();
}
this._probeLegacySystemInfo(pf);
return Promise.resolve();
}
private static _probeWeChat(pf: PlatformApi): Promise<void> {
let chain = Promise.resolve();
if (pf.getDeviceBenchmarkInfo) {
chain = new Promise<void>((resolve) => {
pf.getDeviceBenchmarkInfo!({
success: (res) => {
if (typeof res.benchmarkLevel === 'number') {
this._signals.benchmarkLevel = res.benchmarkLevel;
}
if (typeof res.modelLevel === 'number') {
this._signals.modelLevel = res.modelLevel;
}
this._signals.probeSource = 'wx.getDeviceBenchmarkInfo';
resolve();
},
fail: () => resolve(),
});
});
}
return chain.then(() => {
const deviceInfo = pf.getDeviceInfo?.();
if (deviceInfo) {
this._signals.miniGameOs = this._readMiniGameOs(deviceInfo.platform, deviceInfo);
if (this._signals.benchmarkLevel == null && typeof deviceInfo.benchmarkLevel === 'number') {
this._signals.benchmarkLevel = deviceInfo.benchmarkLevel;
if (this._signals.probeSource === 'fallback') {
this._signals.probeSource = 'wx.getDeviceInfo';
}
}
const memRaw = deviceInfo.memorySize;
const memMb = typeof memRaw === 'string' ? Number(memRaw) : memRaw;
if (typeof memMb === 'number' && memMb > 0) {
this._signals.memorySizeMb = memMb;
}
}
});
}
private static _probeDouyin(pf: PlatformApi): void {
const info = pf.getSystemInfoSync?.();
if (!info) return;
this._signals.miniGameOs = this._readMiniGameOs(info.platform as string | undefined, info);
const score = info.deviceScore as { overall?: number } | undefined;
if (score && typeof score.overall === 'number') {
this._signals.deviceScoreOverall = score.overall;
this._signals.probeSource = 'tt.deviceScore';
}
}
private static _probeLegacySystemInfo(pf: PlatformApi): void {
const info = pf.getSystemInfoSync?.();
if (!info) return;
this._signals.miniGameOs = this._readMiniGameOs(info.platform as string | undefined, info);
this._signals.probeSource = 'legacy.getSystemInfoSync';
const bench = info.benchmarkLevel;
if (typeof bench === 'number') {
this._signals.benchmarkLevel = bench;
}
const memRaw = info.memorySize;
const memMb = typeof memRaw === 'string' ? Number(memRaw) : memRaw;
if (typeof memMb === 'number' && memMb > 0) {
this._signals.memorySizeMb = memMb as number;
}
const score = info.deviceScore as { overall?: number } | undefined;
if (score && typeof score.overall === 'number') {
this._signals.deviceScoreOverall = score.overall;
this._signals.probeSource = 'tt.deviceScore';
}
}
private static _recomputeAfterProbe(): void {
this._resolved = false;
this.resolve();
}
private static _collectWebSignals(): Partial<DeviceCapabilitySignals> {
const partial: Partial<DeviceCapabilitySignals> = {
platform: String(sys.platform),
os: String(sys.os),
miniGameOs: 'unknown',
probeSource: 'fallback',
};
const mem = (sys as unknown as { deviceMemory?: number }).deviceMemory;
if (typeof mem === 'number' && mem > 0) {
partial.deviceMemoryGb = mem;
partial.probeSource = 'web.deviceMemory';
}
if (typeof navigator !== 'undefined') {
const cores = navigator.hardwareConcurrency;
if (typeof cores === 'number' && cores > 0) {
partial.hardwareConcurrency = cores;
if (partial.probeSource === 'fallback') {
partial.probeSource = 'web.hardwareConcurrency';
}
}
}
return partial;
}
private static _readMiniGameOs(platform: string | undefined, info: Record<string, unknown>): MiniGameOs {
const fromPlatform = this._parseOsToken(platform);
if (fromPlatform !== 'unknown') return fromPlatform;
const system = info.system;
if (typeof system === 'string') {
return this._parseOsToken(system);
}
return 'unknown';
}
private static _parseOsToken(raw: string | undefined): MiniGameOs {
if (!raw) return 'unknown';
const lower = raw.toLowerCase();
if (lower.includes('ios') || lower.includes('iphone') || lower.includes('ipad')) return 'ios';
if (lower.includes('android') || lower.includes('harmony')) return 'android';
return 'unknown';
}
private static _isMobileRuntime(s: DeviceCapabilitySignals): boolean {
if (s.miniGameOs === 'ios' || s.miniGameOs === 'android') return true;
return s.os === String(sys.OS.IOS)
|| s.os === String(sys.OS.ANDROID)
|| s.platform === String(sys.Platform.MOBILE_BROWSER);
}
private static _isDesktopRuntime(s: DeviceCapabilitySignals): boolean {
return s.os === String(sys.OS.WINDOWS)
|| s.os === String(sys.OS.OSX)
|| s.os === String(sys.OS.LINUX);
}
/**
* 分档优先级(与官方文档一致):
* 1. 抖音 deviceScore.overall
* 2. 微信 benchmarkLevel(分 iOS / Android 阈值)
* 3. 微信 modelLevel(benchmark 未知)
* 4. deviceMemory / memorySize
* 5. CPU 核数
* 6. 桌面 / 移动 OS 兜底
*/
private static _scoreQuality(s: DeviceCapabilitySignals): DeviceQuality {
const isMobile = this._isMobileRuntime(s);
if (s.deviceScoreOverall != null) {
const q = douyinDeviceScoreToQuality(s.deviceScoreOverall);
if (q) return q;
}
if (s.benchmarkLevel != null) {
const q = wechatBenchmarkToQuality(s.benchmarkLevel, s.miniGameOs);
if (q) return q;
if (s.benchmarkLevel === -1 && s.modelLevel != null) {
const mq = wechatModelLevelToQuality(s.modelLevel);
if (mq) return mq;
}
}
if (s.modelLevel != null && s.benchmarkLevel == null) {
const mq = wechatModelLevelToQuality(s.modelLevel);
if (mq) return mq;
}
if (s.deviceMemoryGb != null) {
return webDeviceMemoryToQuality(s.deviceMemoryGb, isMobile);
}
if (s.memorySizeMb != null) {
return memoryMbToQuality(s.memorySizeMb, isMobile);
}
const cores = s.hardwareConcurrency;
if (cores != null) {
if (cores >= 8) return 'high';
if (cores >= 4) return 'medium';
return 'low';
}
if (this._isDesktopRuntime(s)) return 'high';
if (isMobile) return 'medium';
return 'medium';
}
private static _qualityToMemoryTier(q: DeviceQuality): MemoryTierId {
switch (q) {
case 'low': return 'tier0';
case 'medium': return 'tier1';
case 'high': return 'tier3';
}
}
}