消消方块阵换皮表情
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.

353 lines
13 KiB

1 week ago
/**
* zip Bundle
*
* unzip clearLRU 500ms
* loadBundle cacheList.json
*
* GameLaunch monkey-patch cacheManager
* 1. clearLRUImmediate LRU setTimeout
* 2. unzipAndCacheBundle
*
* CacheManagerPatch
*
* @see Cocos 3.8.8 platforms/minigame/common/engine/cache-manager.js
*/
import { assetManager, path } from "cc";
declare global {
interface Window {
fsUtils?: MinigameFsUtils;
}
}
/** 小游戏引擎适配层注入的文件系统工具(window.fsUtils) */
interface MinigameFsUtils {
fs?: { unlinkSync?(filePath: string): void };
rmdirSync(dirPath: string, recursive?: boolean): Error | null;
makeDirSync(dirPath: string, recursive?: boolean): Error | null;
unzip(zipFilePath: string, targetPath: string, onComplete: (err: Error | null) => void): void;
isOutOfStorage(errMsg: string): boolean;
}
/** 引擎 cacheManager 运行时扩展字段(TS 公开类型未暴露,运行时有) */
interface CacheManagerLike {
cacheDir: string;
autoClear: boolean;
outOfStorage: boolean;
cachedFiles: {
forEach(cb: (val: { bundle: string; url: string; lastTime: number }, key: string) => void): void;
add(key: string, val: { bundle: string; url: string; lastTime: string | number }): void;
remove(key: string): unknown;
};
_isZipFile(url: string): boolean;
_write(): void;
clearLRU(): void;
clearLRUImmediate?(): void;
unzipAndCacheBundle(
id: string,
zipFilePath: string,
cacheBundleRoot: string,
onComplete: (err: Error | null, targetPath?: string) => void,
retryCount?: number
): void;
}
/** 日志前缀,控制台过滤:CacheManagerPatch */
const LOG_TAG = "[CacheManagerPatch]";
/** 存储不足时 unzip 最大重试次数(每次重试前同步清 1/3 LRU) */
const MAX_UNZIP_STORAGE_RETRIES = 5;
/** 解压目标目录后缀,避免同毫秒多次解压路径冲突 */
let unzipPathSuffix = 0;
function log(message: string, detail?: unknown): void {
if (detail !== undefined) {
console.log(LOG_TAG, message, detail);
} else {
console.log(LOG_TAG, message);
}
}
function warn(message: string, detail?: unknown): void {
if (detail !== undefined) {
console.warn(LOG_TAG, message, detail);
} else {
console.warn(LOG_TAG, message);
}
}
function logError(message: string, detail?: unknown): void {
if (detail !== undefined) {
console.error(LOG_TAG, message, detail);
} else {
console.error(LOG_TAG, message);
}
}
/** 统计 cachedFiles 条目数 */
function countCachedFiles(cm: CacheManagerLike): number {
let count = 0;
cm.cachedFiles.forEach(() => {
count++;
});
return count;
}
/**
* fsUtils
* Web null
*/
function getFsUtils(): MinigameFsUtils | null {
const fsUtils = (globalThis as typeof globalThis & { fsUtils?: MinigameFsUtils }).fsUtils;
return fsUtils ?? null;
}
/**
* /
* - zip bundle rmdirSync
* - / zip unlinkSync
*/
function removePathOrFileSync(cm: CacheManagerLike, originUrl: string, localPath: string): boolean {
const fsUtils = getFsUtils();
if (!fsUtils) {
warn("removePathOrFileSync: fsUtils missing", { originUrl, localPath });
return false;
}
if (cm._isZipFile(originUrl)) {
if (cm._isZipFile(localPath)) {
try {
fsUtils.fs?.unlinkSync?.(localPath);
log("removed file (zip)", { originUrl: path.basename(originUrl), localPath });
return true;
} catch (e) {
warn("unlinkSync zip failed", { localPath, error: e });
return false;
}
}
const rmErr = fsUtils.rmdirSync(localPath, true);
if (rmErr) {
warn("rmdirSync failed", { localPath, error: rmErr.message });
return false;
}
log("removed dir (unzipped bundle)", { originUrl: path.basename(originUrl), localPath });
return true;
}
try {
fsUtils.fs?.unlinkSync?.(localPath);
log("removed file", { originUrl: path.basename(originUrl), localPath });
return true;
} catch (e) {
warn("unlinkSync failed", { localPath, error: e });
return false;
}
}
/**
* clearLRU
* 1. cachedFiles使 zip bundle
* 2. lastTime
* 3. 3 1/3< 3
*/
function collectLruCandidates(cm: CacheManagerLike): Array<{ originUrl: string; url: string; lastTime: number }> {
const allCandidates: Array<{ originUrl: string; url: string; lastTime: number }> = [];
let skippedInUse = 0;
cm.cachedFiles.forEach((val, key) => {
let skip = false;
if (cm._isZipFile(key)) {
assetManager.bundles.forEach((bundle) => {
if (bundle.base.indexOf(val.url) !== -1) {
skip = true;
}
});
if (skip) {
skippedInUse++;
return;
}
}
allCandidates.push({ originUrl: key, url: val.url, lastTime: val.lastTime });
});
allCandidates.sort((a, b) => a.lastTime - b.lastTime);
const totalEligible = allCandidates.length;
if (allCandidates.length >= 3) {
allCandidates.length = Math.floor(allCandidates.length / 3);
}
log("collectLruCandidates", {
totalInCache: countCachedFiles(cm),
eligible: totalEligible,
skippedInUseZipBundle: skippedInUse,
toRemove: allCandidates.length,
rule: totalEligible >= 3 ? "remove oldest 1/3" : "remove all eligible",
});
return allCandidates;
}
/**
* cacheManager clearLRUImmediate
* clearLRU cacheList.json
* setTimeout(deferredDelete, deleteInterval)
*/
function installClearLRUImmediate(cm: CacheManagerLike): void {
cm.clearLRUImmediate = function clearLRUImmediate(this: CacheManagerLike): void {
const beforeCount = countCachedFiles(this);
warn("clearLRUImmediate start", {
cacheDir: this.cacheDir,
cachedCount: beforeCount,
outOfStorage: this.outOfStorage,
autoClear: this.autoClear,
});
const caches = collectLruCandidates(this);
if (caches.length === 0) {
warn("clearLRUImmediate: nothing to remove");
return;
}
let diskOk = 0;
let diskFail = 0;
for (let i = 0, l = caches.length; i < l; i++) {
const item = caches[i];
const cacheKey = `${assetManager.utils.getUuidFromURL(item.originUrl)}@native`;
(assetManager as unknown as { files: { remove(key: string): void } }).files.remove(cacheKey);
this.cachedFiles.remove(item.originUrl);
if (removePathOrFileSync(this, item.originUrl, item.url)) {
diskOk++;
} else {
diskFail++;
}
}
this._write();
this.outOfStorage = false;
const afterCount = countCachedFiles(this);
warn("clearLRUImmediate done", {
removedFromIndex: caches.length,
diskOk,
diskFail,
cachedCountBefore: beforeCount,
cachedCountAfter: afterCount,
outOfStorage: this.outOfStorage,
});
};
}
/**
* unzipAndCacheBundle
* clearLRUImmediate
* clearLRU + onComplete(err)
*/
function installUnzipAndCacheBundle(cm: CacheManagerLike, fsUtils: MinigameFsUtils): void {
cm.unzipAndCacheBundle = function unzipAndCacheBundle(
this: CacheManagerLike,
id: string,
zipFilePath: string,
cacheBundleRoot: string,
onComplete: (err: Error | null, targetPath?: string) => void,
retryCount = 0
): void {
const time = Date.now().toString();
const targetPath = `${this.cacheDir}/${cacheBundleRoot}/${time}${unzipPathSuffix++}`;
log("unzip start", {
bundle: cacheBundleRoot,
zipName: path.basename(id),
retry: `${retryCount}/${MAX_UNZIP_STORAGE_RETRIES}`,
zipFilePath,
targetPath,
cachedCount: countCachedFiles(this),
outOfStorage: this.outOfStorage,
});
const mkdirErr = fsUtils.makeDirSync(targetPath, true);
if (mkdirErr) {
logError("makeDirSync failed before unzip", { targetPath, error: mkdirErr.message });
}
fsUtils.unzip(zipFilePath, targetPath, (err) => {
if (err) {
fsUtils.rmdirSync(targetPath, true);
const outOfStorage = fsUtils.isOutOfStorage(err.message);
warn("unzip failed", {
bundle: cacheBundleRoot,
zipName: path.basename(id),
retry: retryCount,
outOfStorage,
message: err.message,
});
if (outOfStorage && retryCount < MAX_UNZIP_STORAGE_RETRIES) {
this.outOfStorage = true;
if (this.autoClear !== false && this.clearLRUImmediate) {
this.clearLRUImmediate();
} else {
warn("skip clearLRUImmediate", {
autoClear: this.autoClear,
hasImmediate: !!this.clearLRUImmediate,
});
}
warn(`unzip out-of-storage → retry ${retryCount + 1}/${MAX_UNZIP_STORAGE_RETRIES}`, {
bundle: cacheBundleRoot,
zipName: path.basename(id),
});
cm.unzipAndCacheBundle(id, zipFilePath, cacheBundleRoot, onComplete, retryCount + 1);
return;
}
if (outOfStorage) {
logError("unzip gave up: out-of-storage retries exhausted", {
bundle: cacheBundleRoot,
zipName: path.basename(id),
maxRetries: MAX_UNZIP_STORAGE_RETRIES,
message: err.message,
});
} else {
logError("unzip gave up: non-storage error (no retry)", {
bundle: cacheBundleRoot,
zipName: path.basename(id),
message: err.message,
});
}
onComplete?.(err);
return;
}
this.cachedFiles.add(id, { bundle: cacheBundleRoot, url: targetPath, lastTime: time });
this._write();
this.outOfStorage = false;
log("unzip ok", {
bundle: cacheBundleRoot,
zipName: path.basename(id),
retry: retryCount,
targetPath,
cachedCount: countCachedFiles(this),
});
onComplete?.(null, targetPath);
});
};
}
/**
* GameLaunch.start
* cacheManager + fsUtils
*/
export function applyCacheManagerPatch(): void {
const cm = assetManager.cacheManager as unknown as CacheManagerLike | null;
const fsUtils = getFsUtils();
if (!cm && !fsUtils) {
log("skip patch: not minigame env (cacheManager & fsUtils both missing)");
return;
}
if (!cm) {
warn("skip patch: assetManager.cacheManager missing");
return;
}
if (!fsUtils) {
warn("skip patch: window.fsUtils missing");
return;
}
if (typeof cm.clearLRUImmediate === "function") {
log("skip patch: already applied");
return;
}
installClearLRUImmediate(cm);
installUnzipAndCacheBundle(cm, fsUtils);
log("patch applied", {
cacheDir: cm.cacheDir,
autoClear: cm.autoClear,
cachedCount: countCachedFiles(cm),
maxUnzipRetries: MAX_UNZIP_STORAGE_RETRIES,
replaced: ["clearLRUImmediate (new)", "unzipAndCacheBundle (override)"],
});
}