no message

This commit is contained in:
2026-06-18 14:07:38 +08:00
parent d393302388
commit 18990deb2d
12 changed files with 910 additions and 116 deletions

View File

@@ -10,6 +10,11 @@
var streamingBase = '';
var bundleManifest = null;
var levelsManifest = null;
var levelsManifestPromise = null;
var assetsCoreLoaded = false;
var assetsCorePromise = null;
var loadedLevelDbShards = new Set();
var levelDbShardPromises = new Map();
var levelPackPromises = new Map();
var loadedLevelIds = new Set();
var levelPrefabsBundlePromise = null;
@@ -54,9 +59,8 @@
return;
}
var existing = am.getBundle('level-prefabs');
if (existing) {
if (onComplete) onComplete(null, existing);
return existing;
if (existing && typeof am.removeBundle === 'function') {
try { am.removeBundle(existing); } catch (e) { /* ignore */ }
}
if (levelPrefabsBundlePromise) {
levelPrefabsBundlePromise.then(function (bundle) {
@@ -131,6 +135,40 @@
}
}
/** Cocos bundle 常请求 import/NN/uuid.jsonzip 内路径带 assets/.../import/ 前缀NN 为 uuid 前 2 位 hex */
var IMPORT_SHARD = '[0-9a-fA-F]{2}';
function resolveImportFileKey(pathname) {
if (!pathname) return null;
var p = String(pathname).replace(/\\/g, '/').replace(/^\/+/, '');
var shardRe = new RegExp('^unity/assets/(level-prefabs|resources|main|internal)/(' + IMPORT_SHARD + ')/(.+)$', 'i');
p = p.replace(shardRe, function (_, bundle, dir, file) {
return 'assets/' + bundle + '/import/' + dir + '/' + file;
});
if (files.has(p)) return p;
var bundleImport = new RegExp('^assets/(level-prefabs|resources|main|internal)/(' + IMPORT_SHARD + ')/([^/]+)$', 'i').exec(p);
if (bundleImport) {
var withImport = 'assets/' + bundleImport[1] + '/import/' + bundleImport[2] + '/' + bundleImport[3];
if (files.has(withImport)) return withImport;
}
var short = new RegExp('^(' + IMPORT_SHARD + ')/(.+)$', 'i').exec(p);
if (short) {
var bases = [
'assets/level-prefabs/import/',
'assets/resources/import/',
'assets/main/import/',
'assets/internal/import/',
'assets/resources/native/',
'assets/main/native/',
'assets/internal/native/',
];
for (var i = 0; i < bases.length; i++) {
var key = bases[i] + short[1] + '/' + short[2];
if (files.has(key)) return key;
}
}
return null;
}
function lookupFile(pathname) {
var keys = [pathname, pathname.replace(/^\.?\//, '')];
var parts = pathname.split('/');
@@ -141,6 +179,8 @@
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
if (files.has(k)) return files.get(k);
var importKey = resolveImportFileKey(k);
if (importKey && files.has(importKey)) return files.get(importKey);
var idx = k.indexOf('StreamingAssets/aa/WebGL/');
if (idx >= 0 && files.has(k.slice(idx + 'StreamingAssets/aa/WebGL/'.length))) {
return files.get(k.slice(idx + 'StreamingAssets/aa/WebGL/'.length));
@@ -151,6 +191,8 @@
}
if (k.indexOf('assets/') === 0 && files.has(k)) return files.get(k);
}
var fallbackKey = resolveImportFileKey(pathname);
if (fallbackKey && files.has(fallbackKey)) return files.get(fallbackKey);
return null;
}
@@ -221,10 +263,20 @@
var u = new URL(String(raw), global.location.href);
var p = u.pathname;
if (files.size > 0) {
var importKey = resolveImportFileKey(p) || resolveImportFileKey(p.replace(/^\/+/, ''));
if (importKey) {
var importVirt = virtualUrlForBundleFile(importKey);
if (importVirt) return importVirt;
}
var rel = relPathFromUrlPath(p);
if (rel) {
var virt = virtualUrlForBundleFile(rel);
if (virt) return virt;
importKey = resolveImportFileKey(rel);
if (importKey) {
importVirt = virtualUrlForBundleFile(importKey);
if (importVirt) return importVirt;
}
}
}
if (p.indexOf(getRootPathname()) === 0) return u.href;
@@ -366,6 +418,12 @@
proto.__tfrhLevelPrefabScript = true;
}
function needsAssetsCore(pathname) {
var p = String(pathname || '');
if (needsLevelPrefabs(p)) return false;
return p.indexOf('assets/') >= 0 || p.indexOf('/assets/') >= 0;
}
function installFetchShim() {
global.fetch = function (input, init) {
input = rewriteEmbeddedUrl(input);
@@ -378,6 +436,19 @@
headers: { 'Content-Type': mimeForPath(p) },
}));
}
if (needsAssetsCore(p)) {
return ensureAssetsCoreLoaded().then(function () {
var hit2 = lookupFile(p);
if (hit2) {
var body2 = /\.(m?js|json)$/i.test(p) ? bytesToText(hit2) : hit2;
return new Response(body2, {
status: 200,
headers: { 'Content-Type': mimeForPath(p) },
});
}
return origFetch(input, init);
});
}
if (needsLevelPrefabs(p)) {
return ensureLevelPackForPath(p).then(function () {
var hit2 = lookupFile(p);
@@ -407,6 +478,18 @@
var origSend = xhr.send;
xhr.send = function () {
var hit = lookupFile(toPath(reqUrl));
if (!hit && needsAssetsCore(reqUrl)) {
ensureAssetsCoreLoaded().then(function () {
hit = lookupFile(toPath(reqUrl));
if (!hit) return origSend.apply(xhr, arguments);
var isText = /\.(m?js|json|txt|xml|atlas|tmx|tsx|vsh|fsh|fnt|plist)$/i.test(toPath(reqUrl))
|| xhr.responseType === 'json'
|| xhr.responseType === 'text'
|| !xhr.responseType;
deliverXhrHit(xhr, isText ? bytesToText(hit) : null, isText ? null : hit);
}).catch(function () { origSend.apply(xhr, arguments); });
return;
}
if (!hit && needsLevelPrefabs(reqUrl)) {
ensureLevelPackForPath(reqUrl).then(function () {
hit = lookupFile(toPath(reqUrl));
@@ -510,32 +593,36 @@
if (onProgress) pending.then(function () { onProgress(1); }).catch(function () {});
return pending;
}
if (!levelsManifest || !levelsManifest.levels || !levelsManifest.levels[levelId]) {
return Promise.reject(new Error('关卡包未在 manifest 中: Level' + levelId));
}
if (!streamingBase) return Promise.reject(new Error('streamingBase 未初始化'));
var entry = levelsManifest.levels[levelId];
var url = joinUrl(streamingBase, 'aa/WebGL/' + entry.bundle);
dispatchLevelsBundleProgress(0);
var promise = fetchBinaryWithProgress(
url,
function (frac) {
dispatchLevelsBundleProgress(frac);
if (onProgress) onProgress(frac);
},
0,
1,
).then(function (buf) {
mergeZipIntoFiles(unzip(buf));
var ok = (entry.files || []).every(function (rel) { return files.has(rel); });
if (!ok) {
throw new Error('关卡包解压不完整: Level' + levelId);
}
loadedLevelIds.add(levelId);
return invalidateLevelPrefabsBundle();
var promise = ensureAssetsCoreLoaded().then(function () {
return ensureLevelsManifestLoaded();
}).then(function () {
dispatchLevelsBundleProgress(1);
if (onProgress) onProgress(1);
if (!levelsManifest || !levelsManifest.levels || !levelsManifest.levels[levelId]) {
return Promise.reject(new Error('关卡包未在 manifest 中: Level' + levelId));
}
if (!streamingBase) return Promise.reject(new Error('streamingBase 未初始化'));
var entry = levelsManifest.levels[levelId];
var url = joinUrl(streamingBase, 'aa/WebGL/' + entry.bundle);
dispatchLevelsBundleProgress(0);
return fetchBinaryWithProgress(
url,
function (frac) {
dispatchLevelsBundleProgress(frac);
if (onProgress) onProgress(frac);
},
0,
1,
).then(function (buf) {
mergeZipIntoFiles(unzip(buf));
var ok = (entry.files || []).every(function (rel) { return files.has(rel); });
if (!ok) {
throw new Error('关卡包解压不完整: Level' + levelId);
}
loadedLevelIds.add(levelId);
return invalidateLevelPrefabsBundle();
}).then(function () {
dispatchLevelsBundleProgress(1);
if (onProgress) onProgress(1);
});
}).catch(function (e) {
levelPackPromises.delete(levelId);
throw e;
@@ -721,6 +808,7 @@
var raw = String(path || '').trim();
var m = /Level(\d+)/.exec(raw);
var lid = m ? parseInt(m[1], 10) : NaN;
var loadPath = raw.indexOf('level-prefabs/') === 0 ? raw : ('level-prefabs/' + raw.replace(/^level-prefabs\//, ''));
var prep = (typeof global.__tfrhEnsureLevelPack === 'function' && !Number.isNaN(lid))
? global.__tfrhEnsureLevelPack(lid)
: Promise.resolve();
@@ -728,17 +816,35 @@
return prep.then(function () {
return invalidateLevelPrefabsBundle();
}).then(function () {
return ensureLevelsManifestLoaded();
}).then(function (manifest) {
var entry = manifest && manifest.levels && manifest.levels[String(lid)];
var uuid = entry && entry.uuid;
return new Promise(function (resolve, reject) {
cc.assetManager.loadBundle('level-prefabs', function (err, bundle) {
if (err || !bundle) {
reject(err || new Error('level-prefabs bundle unavailable'));
return;
}
bundle.load(raw, Prefab, function (e, asset) {
if (e || !asset) reject(e || new Error('missing prefab: ' + raw));
else resolve(asset);
function finish(err, asset) {
if (!err && asset) resolve(asset);
else reject(err || new Error('missing prefab: ' + loadPath));
}
function tryBundleLoad() {
cc.assetManager.loadBundle('level-prefabs', function (err, bundle) {
if (err || !bundle) {
reject(err || new Error('level-prefabs bundle unavailable'));
return;
}
bundle.load(loadPath, Prefab, finish);
});
});
}
if (uuid) {
cc.assetManager.loadAny({ uuid: uuid, type: Prefab }, function (err, asset) {
if (!err && asset) {
resolve(asset);
return;
}
tryBundleLoad();
});
return;
}
tryBundleLoad();
});
});
}
@@ -1060,26 +1166,83 @@
return out;
}
function isZipBytes(bytes) {
return bytes && bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
}
function isJsonText(text) {
if (!text) return false;
var c = text.charCodeAt(0);
return c === 0x7b || c === 0x5b;
}
function decodeBrotliToBytes(ab) {
if (typeof DecompressionStream !== 'undefined') {
return new Response(ab).pipeThrough(new DecompressionStream('brotli'))
.arrayBuffer()
.then(function (buf) { return new Uint8Array(buf); });
}
return Promise.reject(new Error('浏览器不支持 Brotli 解压'));
}
/** OSS 若对 .br 文件再设 Content-Encoding:brfetch 已解压成 zip/json勿二次 Brotli */
function normalizeBinaryPayload(bytes, expectBrotli) {
if (!expectBrotli || isZipBytes(bytes)) {
return Promise.resolve(bytes);
}
var slice = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
return decodeBrotliToBytes(slice).catch(function () {
if (isZipBytes(bytes)) return bytes;
return Promise.reject(new Error('Brotli 解压失败'));
});
}
function fetchBinary(url, onProgress) {
return origFetch(url).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
if (onProgress) onProgress(0.3);
return res.arrayBuffer();
}).then(function (ab) {
if (onProgress) onProgress(0.6);
return new Uint8Array(ab);
return fetchBinaryWithProgress(url, onProgress, 0, 1);
}
function decodeBrotliToText(ab) {
if (typeof DecompressionStream !== 'undefined') {
return new Response(ab).pipeThrough(new DecompressionStream('brotli'))
.arrayBuffer()
.then(function (buf) { return new TextDecoder().decode(buf); });
}
return Promise.reject(new Error('浏览器不支持 Brotli 解压'));
}
function normalizeJsonPayload(ab, expectBrotli) {
var text = new TextDecoder().decode(ab);
if (!expectBrotli || isJsonText(text)) return Promise.resolve(text);
return decodeBrotliToText(ab).catch(function () {
text = new TextDecoder().decode(ab);
if (isJsonText(text)) return text;
return Promise.reject(new Error('Brotli JSON 解压失败'));
});
}
function fetchBinaryWithProgress(url, onProgress, weightStart, weightEnd) {
weightStart = weightStart == null ? 0 : weightStart;
weightEnd = weightEnd == null ? 1 : weightEnd;
return origFetch(url).then(function (res) {
var brUrl = String(url).replace(/\.bundle(\?.*)?$/i, '.bundle.br$1');
if (!/\.br(\?|$)/i.test(brUrl) && brUrl.indexOf('.bundle') >= 0) {
/* brUrl already set */
} else if (/\.json(\?|$)/i.test(url)) {
brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
} else if (!/\.br(\?|$)/i.test(url)) {
brUrl = url + '.br';
} else {
brUrl = url;
}
function readResponse(res, isBrotli) {
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
if (!res.body || !res.body.getReader) {
return res.arrayBuffer().then(function (ab) {
if (onProgress) onProgress(weightEnd);
return new Uint8Array(ab);
var bytes = new Uint8Array(ab);
return normalizeBinaryPayload(bytes, isBrotli).then(function (out) {
if (onProgress) onProgress(weightEnd);
return out;
});
});
}
var reader = res.body.getReader();
@@ -1089,13 +1252,19 @@
function pump() {
return reader.read().then(function (result) {
if (result.done) {
if (onProgress) onProgress(weightEnd);
var out = new Uint8Array(received);
var off = 0;
for (var i = 0; i < chunks.length; i++) {
out.set(chunks[i], off);
off += chunks[i].length;
}
if (isBrotli) {
return normalizeBinaryPayload(out, true).then(function (decoded) {
if (onProgress) onProgress(weightEnd);
return decoded;
});
}
if (onProgress) onProgress(weightEnd);
return out;
}
chunks.push(result.value);
@@ -1108,11 +1277,45 @@
});
}
return pump();
}
return origFetch(brUrl).then(function (res) {
if (res.ok) return readResponse(res, true);
return origFetch(url).then(function (plain) { return readResponse(plain, false); });
}).catch(function () {
return origFetch(url).then(function (plain) { return readResponse(plain, false); });
});
}
function ensureAssetsCoreLoaded(onProgress) {
if (assetsCoreLoaded) return Promise.resolve();
if (assetsCorePromise) return assetsCorePromise;
if (!bundleManifest || !bundleManifest.assetsUrl) {
return Promise.reject(new Error('assets_all manifest 未就绪'));
}
assetsCorePromise = fetchBinaryWithProgress(
bundleManifest.assetsUrl,
onProgress || function () {},
0,
1,
).then(function (buf) {
mergeZipIntoFiles(unzip(buf));
patchSettingsScriptPackages();
assetsCoreLoaded = true;
}).catch(function (e) {
assetsCorePromise = null;
throw e;
});
return assetsCorePromise;
}
global.__tfrhEnsureAssetsCore = function (onProgress) {
return ensureAssetsCoreLoaded(onProgress);
};
function loadStartupBundles(manifest, onProgress) {
onProgress(0.05);
/* internal/main 等引擎启动即需assets_all 须与 scenes 并行首屏加载 */
var scenesP = fetchBinaryWithProgress(manifest.scenesUrl, onProgress, 0.05, 0.42);
var assetsP = fetchBinaryWithProgress(manifest.assetsUrl, onProgress, 0.42, 0.58);
return Promise.all([scenesP, assetsP]).then(function (bufs) {
@@ -1120,6 +1323,7 @@
assertCocosRuntimeLoaded();
mergeZipIntoFiles(unzip(bufs[1]));
patchSettingsScriptPackages();
assetsCoreLoaded = true;
onProgress(0.6);
});
}
@@ -1190,6 +1394,58 @@
});
}
function ensureLevelsManifestLoaded() {
if (levelsManifest) return Promise.resolve(levelsManifest);
if (levelsManifestPromise) return levelsManifestPromise;
if (!streamingBase) return Promise.reject(new Error('streamingBase 未初始化'));
levelsManifestPromise = loadLevelsManifest(streamingBase).then(function (lm) {
levelsManifest = lm;
return lm;
});
return levelsManifestPromise;
}
function findLevelDbShard(index, levelId) {
if (!index || !index.shards) return null;
var id = Number(levelId);
for (var i = 0; i < index.shards.length; i++) {
var s = index.shards[i];
if (id >= s.min && id <= s.max) return s;
}
return null;
}
function ensureLevelDbShardLoaded(levelId) {
var index = global.__tfrhLevelsDbIndex;
if (!index || index.mode !== 'sharded') return Promise.resolve();
var shard = findLevelDbShard(index, levelId);
if (!shard) return Promise.resolve();
var key = shard.file;
if (loadedLevelDbShards.has(key)) return Promise.resolve();
if (levelDbShardPromises.has(key)) return levelDbShardPromises.get(key);
if (!streamingBase) return Promise.reject(new Error('streamingBase 未初始化'));
var url = joinUrl(streamingBase, 'aa/' + key.replace(/^\/+/, ''));
var promise = fetchLevelsDatabaseJson(url).then(function (json) {
loadedLevelDbShards.add(key);
if (!global.__tfrhLevelsDatabaseJson) {
global.__tfrhLevelsDatabaseJson = { version: index.version || 2, levels: {} };
}
var merged = global.__tfrhLevelsDatabaseJson.levels || {};
var incoming = (json && json.levels) || {};
Object.keys(incoming).forEach(function (k) { merged[k] = incoming[k]; });
global.__tfrhLevelsDatabaseJson.levels = merged;
}).catch(function (e) {
levelDbShardPromises.delete(key);
throw e;
});
levelDbShardPromises.set(key, promise);
return promise;
}
global.__tfrhEnsureLevelDbShard = function (levelId) {
return ensureLevelDbShardLoaded(levelId);
};
function loadBundleManifest(config) {
var base = resolveStreamingBase(config);
var catalogUrl = joinUrl(base, 'aa/catalog.json');
@@ -1198,16 +1454,13 @@
return res.json();
}).then(function (catalog) {
var bundles = parseCatalogBundles(catalog);
return loadLevelsManifest(base).then(function (lm) {
levelsManifest = lm;
var manifest = {
scenesUrl: joinUrl(base, 'aa/WebGL/' + bundles.scenes),
assetsUrl: joinUrl(base, 'aa/WebGL/' + bundles.assets),
bundleNames: bundles,
};
bundleManifest = manifest;
return manifest;
});
var manifest = {
scenesUrl: joinUrl(base, 'aa/WebGL/' + bundles.scenes),
assetsUrl: joinUrl(base, 'aa/WebGL/' + bundles.assets),
bundleNames: bundles,
};
bundleManifest = manifest;
return manifest;
});
}
@@ -1268,36 +1521,33 @@
packageRoot = getPackageRoot();
installEmbeddedPathFix();
function resolveLevelsDatabaseUrl(config) {
if (global.__tfrhLevelsDatabaseUrl) return global.__tfrhLevelsDatabaseUrl;
function resolveLevelsDbBase(config) {
var sa = config && config.streamingAssetsUrl;
if (sa && /^https?:\/\//i.test(sa)) {
var cdnRoot = String(sa).replace(/\/StreamingAssets\/?$/i, '');
return joinUrl(cdnRoot, 'levels-database.json');
return String(sa).replace(/\/StreamingAssets\/?$/i, '');
}
return joinUrl(packageRoot || getPackageRoot(), 'levels-database.json');
return packageRoot || getPackageRoot();
}
function decodeBrotliToText(ab) {
if (typeof DecompressionStream !== 'undefined') {
return new Response(ab).pipeThrough(new DecompressionStream('brotli'))
.arrayBuffer()
.then(function (buf) { return new TextDecoder().decode(buf); });
}
return Promise.reject(new Error('浏览器不支持 Brotli 解压'));
function resolveLevelsDatabaseUrl(config) {
if (global.__tfrhLevelsDatabaseUrl) return global.__tfrhLevelsDatabaseUrl;
return joinUrl(resolveLevelsDbBase(config), 'levels-database.json');
}
function resolveLevelsDbIndexUrl(config) {
if (global.__tfrhLevelsDbIndexUrl) return global.__tfrhLevelsDbIndexUrl;
return joinUrl(resolveLevelsDbBase(config), 'levels-db-index.json');
}
function fetchLevelsDatabaseJson(url) {
var brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
return origFetch(brUrl).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
var enc = (res.headers.get('content-encoding') || '').toLowerCase();
if (enc === 'br' || /\.br(\?|$)/i.test(brUrl)) {
return res.arrayBuffer().then(decodeBrotliToText).then(function (text) {
return res.arrayBuffer().then(function (ab) {
return normalizeJsonPayload(ab, true).then(function (text) {
return JSON.parse(text);
});
}
return res.json();
});
}).catch(function () {
return origFetch(url).then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
@@ -1306,14 +1556,20 @@
});
}
function prefetchMainSiteLevelDb(config) {
var url = resolveLevelsDatabaseUrl(config);
global.__tfrhLevelsDatabaseUrl = url;
if (!url || global.__tfrhLevelsDatabaseJson) return Promise.resolve();
return fetchLevelsDatabaseJson(url).then(function (json) {
global.__tfrhLevelsDatabaseJson = json;
function prefetchLevelDbIndex(config) {
var indexUrl = resolveLevelsDbIndexUrl(config);
var legacyUrl = resolveLevelsDatabaseUrl(config);
global.__tfrhLevelsDbIndexUrl = indexUrl;
global.__tfrhLevelsDatabaseUrl = legacyUrl;
if (global.__tfrhLevelsDbIndex) return Promise.resolve();
return fetchLevelsDatabaseJson(indexUrl).then(function (json) {
if (json && json.mode === 'sharded') {
global.__tfrhLevelsDbIndex = json;
return;
}
throw new Error('非分片索引');
}).catch(function (e) {
console.warn('[mstest5] 关卡库预取失败', url, e);
console.warn('[mstest5] 关卡库索引预取失败,启动时将由 LevelDatabase 回退', indexUrl, e);
});
}
@@ -1339,7 +1595,7 @@
onProgress = onProgress || function () {};
onProgress(0.02);
resolveStreamingBase(config);
return prefetchMainSiteLevelDb(config).then(function () {
return prefetchLevelDbIndex(config).then(function () {
return shouldUseBundlePackFlow(config);
}).then(function (useBundles) {
if (!useBundles) {