MatchMaster/assets/Script/module/Tool/BlockAssetManager.ts
COMPUTER\EDY 4a352390b8 更新
2026-08-27 19:31:20 +08:00

682 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { allBlockTexturePaths, BLOCK_TEXTURE_TRIMS, collectBlockTexturePaths } from "./BlockTexturePaths";
declare const wx: any;
declare const tt: any;
type ImageSource = "内存复用" | "本地缓存" | "临时缓存" | "远程下载";
interface BlockWindowOptions {
onRemoteRequired?: () => void;
currentLevel?: number;
nextLevel?: number;
stage?: string;
}
interface BlockCacheCheck {
cached: number;
temporary: number;
missing: number;
complete: boolean;
}
interface BlockDownloadJob {
path: string;
priority: number;
background: boolean;
started: boolean;
resolve: () => void;
reject: (error: any) => void;
}
/** Block 原始 PNG:版本化磁盘缓存与 SpriteFrame/纹理内存分开管理。 */
export default class BlockAssetManager {
static readonly instance = new BlockAssetManager();
static readonly CACHE_KEY = "block_image_cache_v1";
private static readonly BACKGROUND_CONCURRENCY = 8;
// Block 自己调度直存下载:后台最多 8 路,进关可使用总计 10 路。
private static readonly FOREGROUND_DOWNLOAD_LIMIT = 10;
private static readonly BACKGROUND_RETRY_MS = 10000;
private ready: Promise<void> = null;
private version: string = null;
private imageBase = "";
private paths = allBlockTexturePaths();
private frames = new Map<string, cc.SpriteFrame>();
private loading = new Map<string, Promise<void>>();
private fileDownloads = new Map<string, { promise: Promise<void>, job: BlockDownloadJob }>();
private downloadQueue: BlockDownloadJob[] = [];
private activeFileDownloads = 0;
private activeBackgroundDownloads = 0;
private reportSource: (path: string, source: ImageSource) => void = null;
private backgroundDownloads = new Set<string>();
private windowPaths = new Set<string>();
private committedPaths = new Set<string>();
private activePaths = new Set<string>();
private requestId = 0;
private foregroundLoads = 0;
private backgroundRunning = false;
private backgroundTimer: any = null;
private backgroundStarted = false;
private initialize(): Promise<void> {
if (!this.ready) {
this.ready = this.selectVersion().then(() => {
const previous = this.readCacheState();
if (this.getFileSystem() && (previous.version !== this.version || previous.baseUrl !== this.imageBase)) {
this.writeCacheState(this.paths.length, 0, false);
}
}).catch(error => {
this.ready = null;
throw error;
});
}
return this.ready;
}
/** 一次启动只选择一个版本,切场景/回到前台不更换正在使用的缓存命名空间。 */
private async selectVersion(): Promise<string> {
if (this.version !== null) return this.version;
const config = cc.fx.GameConfig;
const base = config.BLOCK_IMAGE_BASE_URL;
const defaultVersion = config.BLOCK_IMAGE_VERSION;
if (!/^https?:\/\//.test(base) || !this.isVersion(defaultVersion)) {
throw new Error("Block 图片地址或默认版本配置无效");
}
this.imageBase = base.replace(/\/+$/, "") + "/";
const sdk = typeof wx !== "undefined" && wx && wx.request ? wx
: typeof tt !== "undefined" && tt && tt.request ? tt : null;
const previous = this.readCacheState();
const fallback = previous.baseUrl === this.imageBase && this.isVersion(previous.version)
? previous.version : defaultVersion;
const url = this.imageBase + "version.json?_t=" + Date.now();
// console.log(`[Block图片][版本检查] 开始,${url}`);
this.version = await new Promise<string>(resolve => {
let finished = false;
let request: any = null;
const finish = (error: any, version?: string) => {
if (finished) return;
finished = true;
clearTimeout(timer);
// if (error) cc.warn("Block 版本检查失败,本次使用已知版本:", fallback, error);
// if (error) console.log(`[Block图片][版本检查] 失败,回退到版本=${fallback}`, error);
// else console.log(`[Block图片][版本检查] 成功,版本=${version}`);
resolve(version || fallback);
};
const timer = setTimeout(() => {
finish(new Error("Block 版本检查超时"));
if (request && request.abort) request.abort();
}, 5000);
const accept = (error: any, data: any) => {
try {
if (error) throw error;
if (typeof data === "string") data = JSON.parse(data);
if (!data || !this.isVersion(data.version)) throw new Error("Block 版本清单无效");
finish(null, data.version);
} catch (error) { finish(error); }
};
try {
// 版本 JSON 不写 Cocos 文件缓存;CDN 也应设置不缓存。
if (sdk) {
request = sdk.request({
url, method: "GET", timeout: 5000, header: { "Cache-Control": "no-cache" },
success: (response: any) => accept(response.statusCode === 200 ? null
: new Error("Block 版本 HTTP " + response.statusCode), response.data),
fail: (error: any) => finish(error)
});
} else {
// 2.4.15 运行时提供此 XHR 接口,项目旧版类型声明未包含。
request = (cc.assetManager.downloader as any).downloadFile(url,
{ responseType: "json", timeout: 5000, header: { "Cache-Control": "no-cache" } }, null, accept);
}
} catch (error) { finish(error); }
});
return this.version;
}
private isVersion(value: any): boolean {
return typeof value === "string" && value.length <= 32 && /^\d+\.\d+\.\d+$/.test(value);
}
private readCacheState(): any {
try {
return JSON.parse(cc.sys.localStorage.getItem(BlockAssetManager.CACHE_KEY)) || {};
} catch (error) { return {}; }
}
/** 当前场景还在显示时保留它的图片,切场景销毁后再释放旧关独占资源。 */
async prepareWindow(current: any[], next: any[], options: BlockWindowOptions = {}): Promise<boolean> {
const currentPaths = collectBlockTexturePaths(current);
const nextPaths = collectBlockTexturePaths(next);
const paths = new Set(currentPaths.concat(nextPaths));
const id = ++this.requestId;
const stage = options.stage || "进关";
const currentLabel = "当前关" + (options.currentLevel || "");
const nextLabel = "下一关" + (options.nextLevel || "");
// 同时存在多个进关请求时,只通知最新窗口;共享加载失败后的远程重试也走这里。
this.reportSource = (path, source) => {
if (id !== this.requestId || !paths.has(path)) return;
const levels = [currentPaths.indexOf(path) >= 0 ? currentLabel : "",
nextPaths.indexOf(path) >= 0 ? nextLabel : ""].filter(Boolean).join(" / ");
// console.log(`[Block图片][${stage}来源] ${levels} ${path}.png -> ${source},版本=${this.version}`);
if (source === "远程下载" && options.onRemoteRequired) options.onRemoteRequired();
};
this.windowPaths = paths;
this.foregroundLoads++;
this.releaseUnused();
try {
await this.initialize();
if (id !== this.requestId) return false;
this.ensureUserCacheFolder();
// console.log(`[Block图片][${stage}检查] ${currentLabel} / ${nextLabel},版本=${this.version},共${paths.size}张`);
const names = Array.from(paths);
// 限制前台并发,后台队列在此期间让出下载机会。
for (let i = 0; i < names.length; i += 4) {
if (id !== this.requestId) return false;
const results = await Promise.all(names.slice(i, i + 4).map(name =>
this.loadFrame(name).then(() => null, error => error)));
const error = results.find(result => result != null);
if (error) throw error;
}
if (id !== this.requestId) return false;
this.committedPaths = paths;
return true;
} catch (error) {
if (id !== this.requestId) return false;
this.windowPaths = this.committedPaths;
throw error;
} finally {
if (id === this.requestId) this.reportSource = null;
this.foregroundLoads--;
this.pumpDownloadQueue();
this.releaseUnused();
}
}
activateScene(blocks: any[]): void {
const paths = collectBlockTexturePaths(blocks);
paths.forEach(path => this.getFrame(path));
this.activePaths = new Set(paths);
this.releaseUnused();
}
releaseScene(): void {
this.activePaths.clear();
this.releaseUnused();
}
getFrame(path: string): cc.SpriteFrame {
const frame = this.frames.get(path);
if (!frame || !cc.isValid(frame)) throw new Error("方块图片尚未就绪: " + path);
return frame;
}
/** 调试时可核对是否只有当前关、下一关,以及尚未销毁的旧场景资源。 */
getLoadedPaths(): string[] {
return Array.from(this.frames.keys()).sort();
}
private releaseUnused(): void {
this.frames.forEach((frame, path) => {
if (this.windowPaths.has(path) || this.committedPaths.has(path) || this.activePaths.has(path)) return;
this.frames.delete(path);
// 动态 SpriteFrame 没有导入依赖表,必须显式释放持有的纹理引用。
const texture = frame.getTexture();
frame.destroy();
texture.decRef();
this.clearImageCaches(path);
});
}
private loadFrame(path: string): Promise<void> {
if (this.frames.has(path) || this.loading.has(path)) {
if (this.reportSource) this.reportSource(path, this.getImageSource(path));
return this.loading.get(path) || Promise.resolve();
}
const promise = this.loadTextureWithRetry(path).then(texture => {
// 在任何 Sprite 使用前禁用合图,确保独立小图可以独立释放。
texture.packable = false;
texture.addRef();
const trim = BLOCK_TEXTURE_TRIMS[path];
// 保留原图的裁剪布局;换图时需保持画布尺寸和透明边距兼容。
const frame = trim && texture.width === trim[6] && texture.height === trim[7]
? new cc.SpriteFrame(texture, cc.rect(trim[0], trim[1], trim[2], trim[3]), false,
cc.v2(trim[4], trim[5]), cc.size(trim[6], trim[7]))
: new cc.SpriteFrame(texture);
frame.name = path;
this.frames.set(path, frame);
this.releaseUnused();
}).then(() => {
this.loading.delete(path);
}, error => {
this.loading.delete(path);
throw error;
});
this.loading.set(path, promise);
return promise;
}
private async loadTextureWithRetry(path: string): Promise<cc.Texture2D> {
const url = this.imageUrl(path);
const load = async (reload: boolean): Promise<cc.Texture2D> => {
const source: ImageSource = reload ? "远程下载" : this.getImageSource(path);
if (this.reportSource) this.reportSource(path, source);
const remote = source === "远程下载";
// if (remote) console.log(`[Block图片][远程加载] ${this.backgroundDownloads.has(path) ? "等待后台下载" : "开始"} `
// + `${path}.png,进关${reload ? "重试" : ""},${url}`);
try {
let loadUrl = source === "本地缓存" ? this.getPersistentFile(path)
: source === "临时缓存" ? this.getTempFile(path) : url;
if (remote && this.canUseUserCache()) {
await this.downloadToUserCache(path, 10);
loadUrl = this.userFilePath(path);
}
const texture = await new Promise<cc.Texture2D>((resolve, reject) => {
const direct = loadUrl !== url;
cc.assetManager.loadRemote(loadUrl,
{
ext: ".png", priority: 10, cacheEnabled: !direct, reload: !direct && reload,
__cacheBundleRoot__: direct ? undefined : "Block",
maxConcurrency: BlockAssetManager.FOREGROUND_DOWNLOAD_LIMIT,
// 本地读取失败由外层显式重试;直存下载失败也只立即重试一次。
maxRetryCount: direct ? 0 : remote ? cc.assetManager.downloader.maxRetryCount : 0
},
(err, result: cc.Texture2D) => err || !result
? reject(err || new Error("Block 图片为空: " + path)) : resolve(result));
});
// if (remote) console.log(`[Block图片][远程加载] 成功 ${path}.png,已直存并加载,${url}`);
return texture;
} catch (error) {
// if (remote) console.log(`[Block图片][远程加载] 失败 ${path}.png,${url}`, error);
throw error;
}
};
try {
return await load(false);
} catch (error) {
// 缓存损坏也要重下,不能仅凭文件存在/完成标记一直命中坏文件。
// console.log(`[Block图片][加载重试] ${path}.png,清理无效缓存后从远程重试`, error);
// this.invalidateFile(path);
return load(true);
}
}
private getFileSystem(): any {
if (typeof wx !== "undefined" && wx && wx.getFileSystemManager) return wx.getFileSystemManager();
if (typeof tt !== "undefined" && tt && tt.getFileSystemManager) return tt.getFileSystemManager();
return null;
}
private getDownloadSdk(): any {
if (typeof wx !== "undefined" && wx && wx.downloadFile) return wx;
if (typeof tt !== "undefined" && tt && tt.downloadFile) return tt;
return null;
}
private userCacheRoot(): string {
const sdk = this.getDownloadSdk();
const root = sdk && sdk.env && sdk.env.USER_DATA_PATH;
return typeof root === "string" && root ? root.replace(/\/$/, "") + "/Block/" + this.version : "";
}
private userFilePath(path: string): string {
const root = this.userCacheRoot();
return root ? root + "/" + path + ".png" : "";
}
private canUseUserCache(): boolean {
const fs = this.getFileSystem();
return !!(this.version && this.userCacheRoot() && fs && fs.mkdirSync && fs.renameSync && fs.unlinkSync);
}
private ensureUserCacheFolder(): void {
const fs = this.getFileSystem();
const folder = this.userCacheRoot();
if (!folder || !fs) return;
try {
if (fs.statSync(folder).isDirectory()) return;
} catch (error) { /* missing */ }
try {
fs.mkdirSync(folder, true);
} catch (error) {
// 并发创建时允许另一个调用先成功;权限/同名文件错误必须继续抛出。
try { if (fs.statSync(folder).isDirectory()) return; } catch (ignored) { /* keep original */ }
// console.log(`[Block图片][用户缓存] 创建目录失败 ${folder}`, error);
throw error;
}
}
private fileExists(path: string): boolean {
if (typeof path !== "string" || !path) return false;
try {
const stat = this.getFileSystem().statSync(path);
return stat.isFile() && stat.size > 0;
} catch (error) {
return false;
}
}
private imageUrl(path: string): string {
if (this.paths.indexOf(path) < 0) throw new Error("未知的 Block 图片: " + path);
return this.imageBase + path + ".png?v=" + this.version;
}
private getPersistentFile(path: string): string {
const direct = this.userFilePath(path);
if (this.fileExists(direct)) return direct;
const cache = cc.assetManager.cacheManager as any;
if (!cache || !this.getFileSystem()) return "";
const url = this.imageUrl(path);
const local = cache.getCache(url);
if (this.fileExists(local)) return local;
if (local) {
cache.removeCache(url);
this.clearDownloadedFile(url);
}
return "";
}
private hasLocalFile(path: string): boolean {
const cache = cc.assetManager.cacheManager as any;
if (!this.getFileSystem()) return false;
if (this.getPersistentFile(path)) return true;
if (!cache) return false;
const url = this.imageUrl(path);
const temp = cache.getTemp(url);
if (temp && !this.fileExists(temp)) {
cache.tempFiles.remove(url);
this.clearDownloadedFile(url);
}
return false;
}
private getTempFile(path: string): string {
if (!this.getFileSystem()) return "";
const cache = cc.assetManager.cacheManager as any;
const url = this.imageUrl(path);
const temp = cache && cache.getTemp(url);
if (this.fileExists(temp)) return temp;
const native = (cc.assetManager as any)._files.get(url + "@native");
if (this.fileExists(native)) return native;
if (native) this.clearDownloadedFile(url);
return "";
}
private getImageSource(path: string): ImageSource {
if (this.frames.has(path)) return "内存复用";
if (this.hasLocalFile(path)) return "本地缓存";
if (this.getTempFile(path)) return "临时缓存";
return "远程下载";
}
private invalidateFile(path: string): void {
const url = this.imageUrl(path);
this.deleteFile(this.userFilePath(path));
this.deleteFile(this.userFilePath(path) + ".tmp");
const cache = cc.assetManager.cacheManager as any;
if (cache) {
cache.removeCache(url);
cache.tempFiles && cache.tempFiles.remove(url);
}
this.clearImageCaches(path);
}
private deleteFile(path: string): void {
if (!path) return;
try { this.getFileSystem().unlinkSync(path); } catch (error) { /* already absent */ }
}
private clearImageCaches(path: string): void {
const url = this.imageUrl(path);
this.clearDownloadedFile(url);
this.clearDownloadedFile(this.userFilePath(path));
}
private clearDownloadedFile(url: string): void {
// 2.4.15 运行时提供这两个缓存,项目的 creator.d.ts 未声明它们。
const manager = cc.assetManager as any;
if (!url) return;
manager._files && manager._files.remove(url + "@native");
manager._parsed && manager._parsed.remove(url + "@native");
}
/** 同一路径共享任务;排队中的后台任务被进关请求命中时立即提升优先级。 */
private downloadToUserCache(path: string, priority: number): Promise<void> {
if (this.fileExists(this.userFilePath(path))) return Promise.resolve();
const existing = this.fileDownloads.get(path);
if (existing) {
if (priority > existing.job.priority) existing.job.priority = priority;
if (priority > 0 && !existing.job.started) existing.job.background = false;
this.pumpDownloadQueue();
return existing.promise;
}
this.ensureUserCacheFolder();
let resolve: () => void;
let reject: (error: any) => void;
const promise = new Promise<void>((ok, fail) => { resolve = ok; reject = fail; });
const job: BlockDownloadJob = {
path, priority, background: priority < 0, started: false,
resolve: resolve!, reject: reject!
};
this.fileDownloads.set(path, { promise, job });
this.downloadQueue.push(job);
this.pumpDownloadQueue();
return promise;
}
private pumpDownloadQueue(): void {
this.downloadQueue.sort((a, b) => b.priority - a.priority);
while (this.activeFileDownloads < BlockAssetManager.FOREGROUND_DOWNLOAD_LIMIT) {
const index = this.downloadQueue.findIndex(job => !job.background
|| (this.foregroundLoads === 0
&& this.activeBackgroundDownloads < BlockAssetManager.BACKGROUND_CONCURRENCY));
if (index < 0) return;
const job = this.downloadQueue.splice(index, 1)[0];
job.started = true;
this.activeFileDownloads++;
if (job.background) this.activeBackgroundDownloads++;
this.startUserDownload(job);
}
}
private startUserDownload(job: BlockDownloadJob): void {
const sdk = this.getDownloadSdk();
const fs = this.getFileSystem();
const url = this.imageUrl(job.path);
const target = this.userFilePath(job.path);
const temporary = target + ".tmp";
this.deleteFile(temporary);
let finished = false;
const finish = (error?: any) => {
if (finished) return;
finished = true;
this.activeFileDownloads--;
if (job.background) this.activeBackgroundDownloads--;
this.fileDownloads.delete(job.path);
if (error) {
this.deleteFile(temporary);
job.reject(error);
} else {
job.resolve();
}
this.pumpDownloadQueue();
};
try {
sdk.downloadFile({
url, filePath: temporary,
success: (response: any) => {
try {
if (!response || response.statusCode !== 200 || !this.fileExists(temporary)) {
throw new Error("Block 图片 HTTP " + (response && response.statusCode));
}
this.deleteFile(target);
fs.renameSync(temporary, target);
if (!this.fileExists(target)) throw new Error("Block 图片直存失败: " + target);
finish();
} catch (error) { finish(error); }
},
fail: (error: any) => finish(error || new Error("Block 图片下载失败: " + job.path))
});
} catch (error) { finish(error); }
}
/** 兼容升级前已经下载到 Cocos 临时目录的文件,直接复制到用户版本目录。 */
private copyToUserCache(path: string, source: string): Promise<void> {
if (this.fileExists(this.userFilePath(path))) return Promise.resolve();
this.ensureUserCacheFolder();
const fs = this.getFileSystem();
const target = this.userFilePath(path);
const temporary = target + ".tmp";
this.deleteFile(temporary);
return new Promise<void>((resolve, reject) => fs.copyFile({
srcPath: source, destPath: temporary,
success: () => {
try {
this.deleteFile(target);
fs.renameSync(temporary, target);
if (!this.fileExists(target)) throw new Error("Block 临时图片迁移失败: " + target);
resolve();
} catch (error) { this.deleteFile(temporary); reject(error); }
},
fail: (error: any) => { this.deleteFile(temporary); reject(error); }
}));
}
startBackgroundCache(): void {
this.runBackgroundCache();
}
private runBackgroundCache(): void {
// 浏览器不做全量预缓存;小游戏必须支持用户目录直存。
const hasFileSystem = !!this.getFileSystem();
const hasUserCache = !!this.getDownloadSdk();
if (!hasFileSystem || !hasUserCache) {
// console.log(`[Block图片][后台缓存] 跳过:用户缓存环境不可用,下载接口=${hasUserCache},文件系统=${hasFileSystem}`);
return;
}
if (!this.backgroundStarted) {
this.backgroundStarted = true;
cc.game.on(cc.game.EVENT_SHOW, this.startBackgroundCache, this);
}
if (this.backgroundRunning) {
// console.log("[Block图片][后台缓存] 任务正在运行,不重复启动");
return;
}
if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
this.backgroundTimer = null;
this.backgroundRunning = true;
// console.log(`[Block图片][后台缓存] 启动,资源清单共${this.paths.length}张,补齐缺失图片`);
this.cacheAllFiles().catch(error => {
// console.log("[Block图片][后台缓存] 任务失败,稍后重试", error);
// cc.warn("Block 后台缓存未完成,稍后重试:", error);
return null;
}).then(result => {
this.backgroundRunning = false;
if (result && result.complete) {
// console.log("[Block图片][后台缓存] 本轮结束,全部已持久缓存");
return;
}
const reason = !result ? "任务异常" : result.missing ? `仍缺少${result.missing}张图片`
: `${result.temporary}张旧临时文件待迁移`;
// console.log(`[Block图片][后台缓存] 本轮结束,${reason},10秒后重试`);
this.backgroundTimer = setTimeout(() => this.runBackgroundCache(), BlockAssetManager.BACKGROUND_RETRY_MS);
});
}
private async cacheAllFiles(): Promise<BlockCacheCheck> {
await this.initialize();
if (!this.canUseUserCache()) throw new Error("Block 用户缓存环境不可用");
this.ensureUserCacheFolder();
const paths = this.paths;
const checked = await this.saveCacheState(paths);
if (checked.complete) {
// console.log("[Block图片][后台缓存] 全部本地缓存有效,无需下载");
return checked;
}
const concurrency = BlockAssetManager.BACKGROUND_CONCURRENCY;
// console.log(`[Block图片][后台缓存] 滚动下载队列,最多${concurrency}张并发,进关期间停止派发`);
let index = 0;
let paused = false;
const worker = async () => {
while (index < paths.length) {
while (this.foregroundLoads > 0) {
// if (!paused) console.log("[Block图片][后台缓存] 暂停派发,优先等待进关图片,已发出的请求继续完成");
paused = true;
await this.delay(100);
}
if (paused) {
// console.log("[Block图片][后台缓存] 进关图片处理结束,继续后台缓存");
paused = false;
}
// 每次只领取一张;完成一张就补一张,等待前台期间其他 worker 可能已取完队列。
if (index >= paths.length) return;
const path = paths[index++];
if (this.hasLocalFile(path)) continue;
const url = this.imageUrl(path);
const temp = this.getTempFile(path);
const fs = this.getFileSystem();
if (temp && fs && fs.copyFile) {
try {
await this.copyToUserCache(path, temp);
// console.log(`[Block图片][用户缓存] 已迁移临时文件 ${path}.png -> ${this.userFilePath(path)}`);
} catch (error) {
// console.log(`[Block图片][用户缓存] 临时文件迁移失败 ${path}.png`, error);
// cc.warn("Block 临时图片迁移失败:", path, error);
}
continue;
}
// 极旧环境没有 copyFile 时不依赖 Cocos 临时缓存,直接补一份用户目录文件。
this.backgroundDownloads.add(path);
// console.log(`[Block图片][远程下载] 开始 ${path}.png,后台缓存,在途=${this.backgroundDownloads.size}/${concurrency},${url}`);
try {
await this.downloadToUserCache(path, -10);
// console.log(`[Block图片][远程下载] 成功 ${path}.png,已直存=${this.userFilePath(path)},${url}`);
} catch (error) {
// console.log(`[Block图片][远程下载] 失败 ${path}.png,${url}`, error);
// cc.warn("Block 图片缓存失败,保留未完成状态:", path, error);
} finally {
this.backgroundDownloads.delete(path);
}
await this.delay(0);
}
};
await Promise.all(Array.from({ length: concurrency }, () => worker()));
return this.saveCacheState(paths);
}
private async saveCacheState(paths: string[]): Promise<BlockCacheCheck> {
let cached = 0;
let temporary = 0;
let missing = 0;
for (let i = 0; i < paths.length; i++) {
if (this.hasLocalFile(paths[i])) cached++;
else if (this.getTempFile(paths[i])) temporary++;
else missing++;
// 分批检查,避免一次同步扫描全部文件阻塞游戏帧。
if (i % 8 === 7) await this.delay(0);
}
const complete = paths.length > 0 && cached === paths.length;
this.writeCacheState(paths.length, cached, complete);
console.log(`[Block图片][缓存校验] 版本=${this.version},用户目录=${this.userCacheRoot()},有效持久文件=${cached}/${paths.length},完整落盘=${complete},临时文件=${temporary},缺失=${missing}`);
return { cached, temporary, missing, complete };
}
private writeCacheState(total: number, cached: number, complete: boolean): void {
const state = JSON.stringify({
version: this.version, baseUrl: this.imageBase,
total, cached, complete
});
try {
if (cc.sys.localStorage.getItem(BlockAssetManager.CACHE_KEY) !== state) {
cc.sys.localStorage.setItem(BlockAssetManager.CACHE_KEY, state);
}
} catch (error) {
// cc.warn("Block 缓存状态保存失败:", error);
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}