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; }; 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 | 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 { 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 { 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 { 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((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 { 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 { let chain = Promise.resolve(); if (pf.getDeviceBenchmarkInfo) { chain = new Promise((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 { const partial: Partial = { 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): 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'; } } }