682 lines
31 KiB
TypeScript
682 lines
31 KiB
TypeScript
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));
|
||
}
|
||
}
|