/**
 * 替换 Unity Build/mstest5.loader.js — 保持 createUnityInstance API，内部启动 Cocos。
 * bundle 文件名从 catalog.json 读取（与 Unity Addressables 一致），路径相对包根目录解析。
 */
(function (global) {
  var PRODUCT = '__PRODUCT_NAME__';
  var files = new Map();
  var origFetch = global.fetch.bind(global);
  var packageRoot = '';
  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;

  function primeLevelPrefabsBundleScripts() {
    if (!levelsShellReady()) return;
    var idxHit = files.get('assets/level-prefabs/index.js');
    if (idxHit && !global.__tfrhLevelPrefabsIndexLoaded) {
      injectScript(bytesToText(idxHit));
      global.__tfrhLevelPrefabsIndexLoaded = true;
    }
  }

  function primeResourcesBundleScripts() {
    var idxHit = files.get('assets/resources/index.js');
    if (idxHit && !global.__tfrhResourcesIndexLoaded) {
      injectScript(bytesToText(idxHit));
      global.__tfrhResourcesIndexLoaded = true;
    }
  }

  /** 拆分包模式下须先 loadBundle('resources')，level-prefabs 预制体依赖 resources */
  function ensureResourcesBundleOnCc(cc, origLoadBundle) {
    var am = cc && cc.assetManager;
    if (!am) return Promise.reject(new Error('assetManager unavailable'));
    var existing = am.getBundle('resources');
    if (existing) return Promise.resolve(existing);
    primeResourcesBundleScripts();
    return new Promise(function (resolve, reject) {
      origLoadBundle('resources', null, function (err, bundle) {
        if (err || !bundle) reject(err || new Error('resources bundle unavailable'));
        else resolve(bundle);
      });
    });
  }

  /** 串行注册 level-prefabs，避免 ensureCocos + LevelPrefabLoader 并发 loadBundle 竞态 */
  function loadLevelPrefabsBundleOnce(cc, origLoadBundle, onComplete) {
    var am = cc && cc.assetManager;
    if (!am) {
      if (onComplete) onComplete(new Error('assetManager unavailable'), null);
      return;
    }
    var existing = am.getBundle('level-prefabs');
    if (existing && typeof am.removeBundle === 'function') {
      try { am.removeBundle(existing); } catch (e) { /* ignore */ }
    }
    if (levelPrefabsBundlePromise) {
      levelPrefabsBundlePromise.then(function (bundle) {
        if (onComplete) onComplete(null, bundle);
      }).catch(function (e) {
        if (onComplete) onComplete(e, null);
      });
      return;
    }
    levelPrefabsBundlePromise = ensureResourcesBundleOnCc(cc, origLoadBundle).then(function () {
      if (!levelsShellReady()) {
        return Promise.reject(new Error('level-prefabs shell 不可用（config/index 应在 assets_all）'));
      }
      primeLevelPrefabsBundleScripts();
      return new Promise(function (resolve, reject) {
      var bundleRoot = joinUrl(packageRoot || getPackageRoot(), 'assets/level-prefabs/');
      origLoadBundle('level-prefabs', { baseUrl: bundleRoot }, function (err, bundle) {
        if (err || !bundle) {
          levelPrefabsBundlePromise = null;
          reject(err || new Error('level-prefabs bundle unavailable'));
          return;
        }
        resolve(bundle);
      });
      });
    });
    levelPrefabsBundlePromise.then(function (bundle) {
      if (onComplete) onComplete(null, bundle);
    }).catch(function (e) {
      if (onComplete) onComplete(e, null);
    });
  }

  function getPackageRoot() {
    var scripts = document.getElementsByTagName('script');
    var i;
    for (i = scripts.length - 1; i >= 0; i--) {
      var src = scripts[i].getAttribute('src') || scripts[i].src || '';
      if (src.indexOf('loader.js') >= 0) {
        var u = new URL(src, global.location.href);
        return u.href.replace(/Build\/[^/?#]+\.loader\.js.*$/i, '');
      }
    }
    return new URL('./', global.location.href).href;
  }

  function resolveStreamingBase(config) {
    packageRoot = getPackageRoot();
    var sa = (config && config.streamingAssetsUrl) || 'StreamingAssets';
    // 与 Unity 一致：/unity/StreamingAssets 相对站点根目录，不能相对 packageRoot（否则会 /unity/unity/...）
    if (/^https?:\/\//i.test(sa)) {
      streamingBase = sa;
    } else if (sa.charAt(0) === '/') {
      streamingBase = new URL(sa, global.location.href).href;
    } else {
      streamingBase = new URL(sa, packageRoot).href;
    }
    if (!/\/$/.test(streamingBase)) streamingBase += '/';
    return streamingBase;
  }

  function joinUrl(base, rel) {
    return new URL(String(rel || '').replace(/^\//, ''), base).href;
  }

  function toPath(url) {
    try {
      var u = new URL(url, global.location.href);
      return decodeURIComponent(u.pathname.replace(/^\/+/, ''));
    } catch (e) {
      return String(url || '');
    }
  }

  /** Cocos bundle 常请求 import/NN/uuid.json，zip 内路径带 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('/');
    var n;
    for (n = 1; n <= 6 && n < parts.length; n++) {
      keys.push(parts.slice(-n).join('/'));
    }
    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));
      }
      var ai = k.indexOf('/assets/');
      if (ai >= 0 && files.has('assets/' + k.slice(ai + 8))) {
        return files.get('assets/' + k.slice(ai + 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;
  }

  function mimeForPath(pathname) {
    if (/\.(m?js)$/i.test(pathname)) return 'application/javascript';
    if (/\.json$/i.test(pathname)) return 'application/json';
    if (/\.wasm$/i.test(pathname)) return 'application/wasm';
    return 'application/octet-stream';
  }

  function getRootPathname() {
    var rootPath = new URL(packageRoot || getPackageRoot(), global.location.href).pathname;
    return /\/$/.test(rootPath) ? rootPath : rootPath + '/';
  }

  function needsUnityPrefix(pathname) {
    return /^\/(src|assets|cocos-js)\//.test(pathname) || pathname === '/src/settings.json';
  }

  function relPathFromUrlPath(pathname) {
    var root = getRootPathname();
    if (pathname.indexOf(root) === 0) {
      return pathname.slice(root.length).replace(/^\/+/, '');
    }
    if (needsUnityPrefix(pathname) || /^\/(assets|src)\//.test(pathname)) {
      return pathname.replace(/^\//, '');
    }
    return '';
  }

  function normalizeBundleRelPath(raw) {
    var s = String(raw || '').replace(/\\/g, '/').trim();
    if (!s) return '';
    if (s.indexOf('blob:') === 0) {
      s = s.slice(5).replace(/^https?:\/?\/?/i, '');
      if (s.charAt(0) === '/') s = s.slice(1);
    }
    var parts = s.split('/');
    var out = [];
    for (var i = 0; i < parts.length; i++) {
      var part = parts[i];
      if (!part || part === '.') continue;
      if (part === '..') {
        if (out.length) out.pop();
        continue;
      }
      out.push(part);
    }
    return out.join('/');
  }

  function resolveBundleVirtualUrl(raw) {
    if (!files.size || !raw) return null;
    var norm = normalizeBundleRelPath(raw);
    if (!norm) return null;
    var virt = virtualUrlForBundleFile(norm);
    if (virt) return virt;
    if (norm.indexOf('src/chunks/bundle.js') >= 0) return virtualUrlForBundleFile('src/chunks/bundle.js');
    if (norm.indexOf('src/effect.bin') >= 0) return virtualUrlForBundleFile('src/effect.bin');
    return null;
  }

  function fixEmbeddedUrl(raw) {
    if (!raw) return raw;
    var virtEarly = resolveBundleVirtualUrl(raw);
    if (virtEarly) return virtEarly;
    try {
      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;
      if (needsUnityPrefix(p)) {
        return joinUrl(packageRoot || getPackageRoot(), p.replace(/^\//, ''));
      }
    } catch (e) { /* ignore */ }
    return raw;
  }

  function rewriteEmbeddedUrl(input) {
    var raw = typeof input === 'string' ? input : (input && input.url) || '';
    var fixed = fixEmbeddedUrl(raw);
    return fixed === raw ? input : fixed;
  }

  function installEmbeddedPathFix() {
    if (global.__unityEmbeddedPathFixed) return;
    global.__unityEmbeddedPathFixed = true;

    global.fetch = function (input, init) {
      return origFetch(rewriteEmbeddedUrl(input), init);
    };

    var OrigXHR = global.XMLHttpRequest;
    if (OrigXHR && !OrigXHR.__unityPathFixed) {
      global.XMLHttpRequest = function () {
        var xhr = new OrigXHR();
        var origOpen = xhr.open;
        xhr.open = function (method, url) {
          return origOpen.call(xhr, method, fixEmbeddedUrl(url));
        };
        return xhr;
      };
      global.XMLHttpRequest.__unityPathFixed = true;
    }

    patchElementUrlLoading(HTMLScriptElement, ['src']);
    patchElementUrlLoading(HTMLImageElement, ['src']);
    patchElementUrlLoading(HTMLLinkElement, ['href']);
    patchElementUrlLoading(HTMLVideoElement, ['src', 'poster']);
    patchElementUrlLoading(HTMLAudioElement, ['src']);
    patchElementUrlLoading(HTMLSourceElement, ['src']);
  }

  function patchElementUrlLoading(Ctor, props) {
    if (!Ctor || !Ctor.prototype || Ctor.prototype.__unityUrlLoadingFixed) return;
    var proto = Ctor.prototype;
    var origSetAttr = proto.setAttribute;
    proto.setAttribute = function (name, value) {
      if (value && props.indexOf(name) >= 0) value = fixEmbeddedUrl(value);
      return origSetAttr.call(this, name, value);
    };
    for (var i = 0; i < props.length; i++) {
      patchUrlProperty(proto, props[i]);
    }
    proto.__unityUrlLoadingFixed = true;
  }

  function patchUrlProperty(proto, prop) {
    var key = '__unity' + prop + 'Fixed';
    if (proto[key]) return;
    var desc = Object.getOwnPropertyDescriptor(proto, prop);
    if (!desc || !desc.set) return;
    Object.defineProperty(proto, prop, {
      configurable: true,
      enumerable: desc.enumerable,
      get: desc.get,
      set: function (v) { desc.set.call(this, fixEmbeddedUrl(v)); },
    });
    proto[key] = true;
  }

  var origScriptSrcDesc = null;

  function resolveEmbeddedAssetUrl(raw) {
    if (!raw) return raw;
    var fixed = fixEmbeddedUrl(raw);
    var p = toPath(typeof fixed === 'string' ? fixed : String(raw || ''));
    var rel = relPathFromUrlPath(p) || p.replace(/^\.?\//, '');
    if (lookupFile(p) || (rel && lookupFile(rel))) {
      return virtualUrlForBundleFile(rel) || fixed;
    }
    return fixed;
  }

  function assignScriptSrc(el, raw) {
    var url = resolveEmbeddedAssetUrl(raw);
    if (origScriptSrcDesc && origScriptSrcDesc.set) {
      origScriptSrcDesc.set.call(el, url);
    } else {
      el.setAttribute('src', url);
    }
  }

  function ensureLevelPackForPath(pathname) {
    var lid = levelIdFromPrefabPath(pathname);
    if (!lid) return Promise.resolve();
    return ensureLevelPackLoaded(lid);
  }

  function assignLevelPrefabScriptSrc(el, value) {
    var apply = function () { assignScriptSrc(el, value); };
    if (levelsShellReady() && fileHit(value)) {
      apply();
      return;
    }
    ensureLevelPackForPath(value).then(apply).catch(apply);
  }

  function patchLevelPrefabsScriptLoading() {
    if (!HTMLScriptElement || HTMLScriptElement.prototype.__tfrhLevelPrefabScript) return;
    var proto = HTMLScriptElement.prototype;
    origScriptSrcDesc = Object.getOwnPropertyDescriptor(proto, 'src');
    var origSetAttr = proto.setAttribute;
    proto.setAttribute = function (name, value) {
      if (name === 'src' && needsLevelPrefabs(value)) {
        assignLevelPrefabScriptSrc(this, value);
        return;
      }
      if (value && name === 'src') value = resolveEmbeddedAssetUrl(value);
      return origSetAttr.call(this, name, value);
    };
    var desc = origScriptSrcDesc;
    if (desc && desc.set) {
      Object.defineProperty(proto, 'src', {
        configurable: true,
        enumerable: desc.enumerable,
        get: desc.get,
        set: function (v) {
          if (needsLevelPrefabs(v)) {
            assignLevelPrefabScriptSrc(this, v);
            return;
          }
          desc.set.call(this, resolveEmbeddedAssetUrl(v));
        },
      });
    }
    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);
      var p = toPath(typeof input === 'string' ? input : (input && input.url) || '');
      var hit = lookupFile(p);
      if (hit) {
        var body = /\.(m?js|json)$/i.test(p) ? bytesToText(hit) : hit;
        return Promise.resolve(new Response(body, {
          status: 200,
          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);
          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);
        });
      }
      return origFetch(input, init);
    };

    var OrigXHR = global.XMLHttpRequest;
    if (OrigXHR) {
      global.XMLHttpRequest = function () {
        var xhr = new OrigXHR();
        var reqUrl = '';
        var origOpen = xhr.open;
        xhr.open = function (method, url) {
          reqUrl = fixEmbeddedUrl(url);
          return origOpen.call(xhr, method, reqUrl);
        };
        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));
              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) 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);
        };
        return xhr;
      };
    }
    patchLevelPrefabsScriptLoading();
  }

  function deliverXhrHit(xhr, text, rawBytes) {
    var responseType = xhr.responseType || '';
    setTimeout(function () {
      try {
        var response = text;
        if (responseType === 'json') {
          try {
            response = typeof text === 'string' ? JSON.parse(text) : text;
          } catch (parseErr) {
            console.warn('[mstest5] XHR json 解析失败', parseErr);
          }
        } else if (responseType === 'arraybuffer' && rawBytes) {
          response = rawBytes.buffer.slice(rawBytes.byteOffset, rawBytes.byteOffset + rawBytes.byteLength);
        }
        Object.defineProperty(xhr, 'readyState', { value: 4 });
        Object.defineProperty(xhr, 'status', { value: 200 });
        Object.defineProperty(xhr, 'responseText', { value: typeof text === 'string' ? text : '' });
        Object.defineProperty(xhr, 'response', { value: response });
        if (xhr.onreadystatechange) xhr.onreadystatechange();
        if (xhr.onload) xhr.onload();
      } catch (e) { /* ignore */ }
    }, 0);
  }

  function needsLevelPrefabs(pathname) {
    var p = String(pathname || '');
    return p.indexOf('level-prefabs') >= 0 || p.indexOf('assets/level-prefabs') >= 0;
  }

  function levelsShellReady() {
    return files.has('assets/level-prefabs/config.json')
      && files.has('assets/level-prefabs/index.js');
  }

  function levelIdFromPrefabPath(pathname) {
    var m = /Level(\d+)/.exec(String(pathname || ''));
    if (m) return m[1];
    if (!levelsManifest || !levelsManifest.levels) return null;
    var norm = String(pathname || '').replace(/\\/g, '/');
    var base = norm.split('/').pop() || norm;
    var id;
    for (id in levelsManifest.levels) {
      if (!Object.prototype.hasOwnProperty.call(levelsManifest.levels, id)) continue;
      var entry = levelsManifest.levels[id];
      var filesList = entry && entry.files;
      if (!filesList || !filesList.length) continue;
      for (var i = 0; i < filesList.length; i++) {
        var rel = String(filesList[i] || '');
        if (norm.indexOf(rel) >= 0 || base === rel.split('/').pop()) return id;
      }
    }
    return null;
  }

  function dispatchLevelsBundleProgress(progress) {
    if (typeof global.__tfrhOnLevelsBundleProgress === 'function') {
      try { global.__tfrhOnLevelsBundleProgress(progress); } catch (e) { /* ignore */ }
    }
    try {
      global.dispatchEvent(new CustomEvent('tfrh-levels-bundle-progress', {
        detail: { progress: progress },
      }));
    } catch (e2) { /* ignore */ }
    try {
      global.dispatchEvent(new CustomEvent('tfrh-level-pack-progress', {
        detail: { progress: progress },
      }));
    } catch (e3) { /* ignore */ }
  }

  function ensureLevelPackLoaded(levelId, onProgress) {
    levelId = String(levelId);
    if (loadedLevelIds.has(levelId)) return Promise.resolve();
    if (levelPackPromises.has(levelId)) {
      var pending = levelPackPromises.get(levelId);
      if (onProgress) pending.then(function () { onProgress(1); }).catch(function () {});
      return pending;
    }
    var promise = ensureAssetsCoreLoaded().then(function () {
      return ensureLevelsManifestLoaded();
    }).then(function () {
      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;
    });
    levelPackPromises.set(levelId, promise);
    return promise;
  }

  /** 按关解压 import 后须丢弃旧 level-prefabs bundle，否则 bundle.load 仍用壳 config */
  function invalidateLevelPrefabsBundle() {
    levelPrefabsBundlePromise = null;
    global.__tfrhLevelPrefabsIndexLoaded = false;
    return withCocosEngine(function (cc) {
      var am = cc && cc.assetManager;
      if (!am) return;
      var bundle = am.getBundle('level-prefabs');
      if (bundle && typeof am.removeBundle === 'function') {
        try { am.removeBundle(bundle); } catch (e) { /* ignore */ }
      }
    });
  }

  global.__tfrhEnsureLevelPack = function (levelId, onProgress) {
    return ensureLevelPackLoaded(levelId, onProgress);
  };

  global.__tfrhInvalidateLevelPrefabsBundle = invalidateLevelPrefabsBundle;

  /** 兼容旧路径：按路径推断 levelId 再下载 */
  function ensureLevelsBundleLoaded(onProgress) {
    return Promise.resolve();
  }

  function prefetchLevelsBundleBackground() {}

  function bytesToText(body) {
    return new TextDecoder().decode(body);
  }

  function injectScript(code, type) {
    var s = document.createElement('script');
    if (type) s.type = type;
    s.charset = 'utf-8';
    s.text = code;
    document.body.appendChild(s);
  }

  function fileHit(src) {
    var base = String(src || '').replace(/^\.\//, '').replace(/^\//, '');
    return lookupFile(base) || lookupFile(src) || files.get(base) || files.get(base.split('/').pop());
  }

  function loadScriptSrc(src, type) {
    return new Promise(function (resolve, reject) {
      var root = packageRoot || getPackageRoot();
      var rel = String(src || '').replace(/^\.\//, '');
      var url = joinUrl(root, rel);
      var s = document.createElement('script');
      if (type) s.type = type;
      s.charset = 'utf-8';
      s.src = url;
      s.onload = function () { resolve(); };
      s.onerror = function () {
        reject(new Error('Failed to load ' + url));
      };
      document.body.appendChild(s);
    });
  }

  function loadScript(src, type) {
    var hit = fileHit(src);
    if (hit && files.size > 0) {
      return Promise.resolve(injectScript(bytesToText(hit), type));
    }
    var root = packageRoot || getPackageRoot();
    var urls = [
      src,
      joinUrl(root, src),
      joinUrl(root, src.replace(/^\.\//, '')),
      '/unity/' + String(src).replace(/^\//, ''),
    ];
    var baseName = String(src).split('/').pop();
    if (baseName === 'cocos-bridge.js') {
      urls.push('/cocos-bridge.js', '/unity/cocos-bridge.js');
    }
    var i = 0;
    function tryNext() {
      if (i >= urls.length) {
        return Promise.reject(new Error(
          'Failed to load ' + src
          + ' (bundle 内无此文件；若 usecdn=true 请上传新版 StreamingAssets/aa/WebGL/*.bundle 到 CDN)',
        ));
      }
      var url = urls[i++];
      return origFetch(url).then(function (res) {
        if (!res.ok) return tryNext();
        return res.text();
      }).then(function (code) {
        if (!code) return tryNext();
        injectScript(code, type);
      }).catch(function () { return tryNext(); });
    }
    return tryNext();
  }

  function ensureSystemBase() {
    var root = packageRoot || getPackageRoot();
    try {
      if (global.System && typeof global.System.config === 'function') {
        global.System.config({ baseURL: root });
      }
    } catch (e) { /* ignore */ }
    // 禁止修改 document <base>，否则会破坏主站 sw.js、manifest 等相对路径
  }

  function virtualUrlForBundleFile(relPath) {
    var hit = lookupFile(relPath) || files.get(relPath);
    if (!hit) return null;
    var cacheKey = '__tfrhBlobUrl:' + relPath;
    if (!global[cacheKey]) {
      global[cacheKey] = URL.createObjectURL(new Blob([hit], { type: mimeForPath(relPath) }));
    }
    return global[cacheKey];
  }

  function patchImportMapCc(map, useLocalDisk) {
    if (!map.imports || !map.imports.cc) return map;
    // bundle 模式：cc.js 仅在 zip 解压后的内存中，不能用磁盘路径
    if (!useLocalDisk) {
      var virtual = virtualUrlForBundleFile('cocos-js/cc.js');
      if (virtual) {
        map.imports.cc = virtual;
        return map;
      }
    }
    var rootHref = packageRoot || getPackageRoot();
    var basePath = new URL(rootHref, global.location.href).pathname;
    if (!/\/$/.test(basePath)) basePath += '/';
    map.imports.cc = basePath + 'cocos-js/cc.js';
    return map;
  }

  function markCocosImportMapReady() {
    global.__tfrhCocosImportMapReady = true;
    try {
      if (global.System && typeof global.System.prepareImport === 'function') {
        return Promise.resolve(global.System.prepareImport());
      }
    } catch (e) { /* ignore */ }
    return Promise.resolve();
  }

  function bindCocosEngine(cc) {
    if (cc) {
      global.__tfrhCocosEngine = cc;
      patchAssetManagerLoadBundle(cc);
    }
    return cc;
  }

  /** loadBundle('level-prefabs') 前先注入 index.js，并保证 levels zip 已解压 */
  function patchAssetManagerLoadBundle(cc) {
    var am = cc && cc.assetManager;
    if (!am || am.__tfrhLoadBundlePatched) return;
    var origLoadBundle = am.loadBundle.bind(am);
    am.__tfrhOrigLoadBundle = origLoadBundle;
    am.loadBundle = function (name, options, onComplete) {
      if (typeof options === 'function') {
        onComplete = options;
        options = null;
      }
      var bundleName = String(name || '');
      if (bundleName.indexOf('level-prefabs') < 0) {
        return origLoadBundle(name, options, onComplete);
      }
      loadLevelPrefabsBundleOnce(cc, origLoadBundle, onComplete);
    };
    am.__tfrhLoadBundlePatched = true;
  }

  /** 覆盖 LevelPrefabLoader：内置 c() 缓存与 bundle 壳不同步，改走已验证的 loadBundle+load 路径 */
  function loadLevelPrefabViaBundle(cc, path) {
    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();
    var Prefab = cc.Prefab;
    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) {
        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();
      });
    });
  }

  function installLevelPrefabLoadFix(cc) {
    if (!cc) return Promise.resolve();
    global.__tfrhLoadLevelPrefab = function (path) {
      return loadLevelPrefabViaBundle(cc, path);
    };
    if (!global.System) return Promise.resolve();
    return global.System.import('chunks:///_virtual/LevelPrefabLoader.ts').then(function (mod) {
      if (!mod) return;
      mod.loadLevelPrefab = global.__tfrhLoadLevelPrefab;
      mod.__tfrhLoadPatched = true;
    }).catch(function () { /* module not ready */ });
  }

  /** cc.game.init 后仅确保 resources；level-prefabs 进关时 loadLevelPrefabsBundleOnce 再下 levels_all */
  function ensureCocosResourcesBundle(cc) {
    if (!cc || !cc.assetManager) return Promise.resolve();
    var origLoadBundle = cc.assetManager.__tfrhOrigLoadBundle;
    if (!origLoadBundle) return Promise.resolve();
    return ensureResourcesBundleOnCc(cc, origLoadBundle);
  }

  function ensureCocosGameRunning(cc) {
    if (!cc || !cc.game) return Promise.resolve();
    if (typeof global.__tfrhSyncEmbeddedCanvasNow === 'function') {
      global.__tfrhSyncEmbeddedCanvasNow();
    }
    if (cc.game.inited) {
      syncEmbeddedCamerasFromLoader(cc);
      return installLevelPrefabLoadFix(cc);
    }
    return cc.game.init({
      debugMode: cc.DebugMode.ERROR,
      settingsPath: 'src/settings.json',
    }).then(function () {
      if (typeof global.__tfrhSyncEmbeddedCanvasNow === 'function') {
        global.__tfrhSyncEmbeddedCanvasNow();
      }
      return ensureCocosResourcesBundle(cc);
    }).then(function () {
      return installLevelPrefabLoadFix(cc);
    }).then(function () {
      return cc.game.run();
    }).then(function () {
      syncEmbeddedCamerasFromLoader(cc);
      if (typeof global.__tfrhSyncEmbeddedCanvas === 'function') {
        global.__tfrhSyncEmbeddedCanvas();
      }
    });
  }

  function withCocosEngine(fn) {
    if (!global.__tfrhCocosImportMapReady) return Promise.resolve();
    var cached = global.__tfrhCocosEngine;
    if (cached) {
      fn(cached);
      return Promise.resolve();
    }
    if (!global.System) return Promise.resolve();
    return global.System.import('cc').then(function (cc) {
      bindCocosEngine(cc);
      fn(cc);
    }).catch(function () { /* cc not ready */ });
  }

  function loadImportMapFromDisk() {
    var root = packageRoot || getPackageRoot();
    return origFetch(joinUrl(root, 'src/import-map.json')).then(function (res) {
      if (!res.ok) throw new Error('HTTP ' + res.status + ' for import-map.json');
      return res.json();
    }).then(function (map) {
      var s = document.createElement('script');
      s.type = 'systemjs-importmap';
      s.charset = 'utf-8';
      s.textContent = JSON.stringify(patchImportMapCc(map, true));
      document.head.appendChild(s);
      return markCocosImportMapReady();
    });
  }

  function loadImportMap() {
    var hit = fileHit('src/import-map.json');
    var mapPromise;
    if (hit && files.size > 0) {
      try {
        mapPromise = Promise.resolve(JSON.parse(bytesToText(hit)));
      } catch (e) {
        mapPromise = Promise.reject(e);
      }
    } else {
      var root = packageRoot || getPackageRoot();
      mapPromise = origFetch(joinUrl(root, 'src/import-map.json')).then(function (res) {
        if (!res.ok) throw new Error('HTTP ' + res.status + ' for import-map.json');
        return res.json();
      });
    }
    return mapPromise.then(function (map) {
      var s = document.createElement('script');
      s.type = 'systemjs-importmap';
      s.charset = 'utf-8';
      s.textContent = JSON.stringify(patchImportMapCc(map, false));
      document.head.appendChild(s);
      return markCocosImportMapReady();
    });
  }

  function assertCocosRuntimeLoaded() {
    if (files.has('index.js') && (files.has('cocos-bridge.js') || files.has('application.js'))) return;
    throw new Error(
      'scenes bundle 不是 Cocos 运行时包（可能 CDN 仍是旧 Unity bundle）。'
      + ' 请执行 deploy-to-001code.sh 并上传 StreamingAssets/aa/WebGL/defaultlocalgroup_scenes_all_*.bundle',
    );
  }

  function ensureCocosDom(canvas) {
    if (!canvas) return;
    canvas.id = 'GameCanvas';
    canvas.tabIndex = 99;
    var inner = document.getElementById('Cocos3dGameContainer');
    if (!inner) {
      inner = document.createElement('div');
      inner.id = 'Cocos3dGameContainer';
      canvas.parentNode.insertBefore(inner, canvas);
      inner.appendChild(canvas);
    }
    inner.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;overflow:hidden;';
    canvas.style.cssText = 'display:block;width:100%;height:100%;';
    installEmbeddedStageLayout(canvas);
  }

  function applyEmbeddedResolution(cc) {
    if (!cc || !cc.view) return;
    var view = cc.view;
    var RES = cc.ResolutionPolicy;
    view.resizeWithBrowserSize(true);
    view.setDesignResolutionSize(960, 600, RES.FIXED_WIDTH);
  }

  function syncEmbeddedCamerasFromLoader(cc) {
    if (!cc || !cc.view || !cc.director) return;
    applyEmbeddedResolution(cc);
    var vis = cc.view.getVisibleSize();
    var halfH = (vis.height > 0 ? vis.height : 600) / 2;
    var scene = cc.director.getScene();
    if (!scene) return;
    var find = cc.find;
    ['UICamera', 'BgCamera', 'Main Camera'].forEach(function (name) {
      var node = find(name, scene);
      if (!node) return;
      var cam = node.getComponent(cc.Camera);
      if (cam) cam.orthoHeight = halfH;
    });
    var mainNode = find('Main Camera', scene);
    if (mainNode) mainNode.setPosition(0, 0, mainNode.position.z);
  }

  function installEmbeddedStageLayout(canvas) {
    if (!canvas || canvas.__tfrhEmbeddedLayout) return;
    canvas.__tfrhEmbeddedLayout = true;
    var stageRoot = canvas.parentElement;
    while (stageRoot && stageRoot !== document.body) {
      if (stageRoot.classList && stageRoot.className.indexOf('unity-stage-root') >= 0) break;
      stageRoot = stageRoot.parentElement;
    }
    if (!stageRoot || stageRoot === document.body) {
      stageRoot = canvas.parentElement;
      while (stageRoot && stageRoot !== document.body) {
        if (stageRoot.clientWidth > 0 && stageRoot.clientHeight > 0) break;
        stageRoot = stageRoot.parentElement;
      }
      if (!stageRoot || stageRoot === document.body) {
        stageRoot = canvas.parentElement;
      }
    }

    var canvasResizeHooked = false;
    var syncRaf = 0;
    var syncing = false;

    function afterCanvasResize(cc) {
      syncEmbeddedCamerasFromLoader(cc);
      if (typeof global.__tfrhSyncHudOrtho === 'function') global.__tfrhSyncHudOrtho();
    }

    function setFrameSizeIfChanged(cc, w, h) {
      if (w <= 0 || h <= 0) return false;
      var frame = cc.view.getFrameSize();
      if (Math.floor(frame.width) === w && Math.floor(frame.height) === h) return false;
      cc.view.setFrameSize(w, h);
      return true;
    }

    function ensureCanvasResizeHook(cc) {
      if (canvasResizeHooked) return;
      canvasResizeHooked = true;
      cc.view.on('canvas-resize', function () {
        // Never call setFrameSize here — it re-triggers canvas-resize and overflows the stack.
        afterCanvasResize(cc);
      });
    }

    function runSyncLayout() {
      var w = Math.floor((stageRoot && stageRoot.clientWidth) || 0);
      var h = Math.floor((stageRoot && stageRoot.clientHeight) || 0);
      if (w <= 0 || h <= 0) return;
      var container = document.getElementById('Cocos3dGameContainer');
      if (container) {
        container.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;overflow:hidden;';
      }
      canvas.style.cssText = 'display:block;width:100%;height:100%;';
      if (stageRoot) {
        stageRoot.style.position = 'relative';
        stageRoot.style.minHeight = '0';
        if (!stageRoot.classList || stageRoot.className.indexOf('unity-stage-root') < 0) {
          stageRoot.style.width = '100%';
          stageRoot.style.height = '100%';
        }
        stageRoot.style.backgroundColor = '#010101';
      }
      if (!global.__tfrhCocosImportMapReady) return;
      if (syncing) return;
      syncing = true;
      withCocosEngine(function (cc) {
        ensureCanvasResizeHook(cc);
        cc.view.resizeWithBrowserSize(true);
        if (setFrameSizeIfChanged(cc, w, h)) {
          /* afterCanvasResize runs via canvas-resize listener */
        } else {
          afterCanvasResize(cc);
        }
      }).finally(function () { syncing = false; });
    }

    function syncLayout() {
      if (syncRaf) return;
      syncRaf = requestAnimationFrame(function () {
        syncRaf = 0;
        runSyncLayout();
      });
    }

    global.__tfrhSyncEmbeddedCanvas = syncLayout;
    global.__tfrhSyncEmbeddedCanvasNow = runSyncLayout;
    runSyncLayout();
    syncLayout();
    if (typeof ResizeObserver !== 'undefined' && stageRoot) {
      var ro = new ResizeObserver(syncLayout);
      ro.observe(stageRoot);
    }
    window.addEventListener('resize', syncLayout);
  }

  function waitInstance(maxMs) {
    maxMs = maxMs || 120000;
    return new Promise(function (resolve, reject) {
      var t0 = Date.now();
      (function tick() {
        var ins = global.unityInstance || global.cocosIns;
        if (ins && global.__tfrhCocosReady) return resolve(ins);
        if (Date.now() - t0 > maxMs) return reject(new Error('Cocos instance timeout'));
        requestAnimationFrame(tick);
      })();
    });
  }

  function probeLocalExtracted() {
    var root = packageRoot || getPackageRoot();
    var url = joinUrl(root, 'index.js');
    // 不用 HEAD：dev server 可能对缺失文件仍返回 200/HTML，误判为本地包
    return origFetch(url, { cache: 'no-cache' }).then(function (res) {
      if (!res.ok) return false;
      return res.text().then(function (txt) {
        if (!txt || /<\s*html/i.test(txt)) return false;
        return /System\.register|application\.js/i.test(txt);
      });
    }).catch(function () { return false; });
  }

  function systemImportEntry(useLocalDisk) {
    if (!useLocalDisk) {
      var hit = fileHit('index.js');
      if (hit) {
        injectScript(bytesToText(hit));
        return Promise.resolve();
      }
    }
    var root = packageRoot || getPackageRoot();
    // 主站嵌入在 /editor.html，不能用相对 ./index.js（会解析到站点根目录）
    var entryUrl = joinUrl(root, 'index.js');
    if (useLocalDisk) {
      entryUrl += '?v=' + Date.now();
    }
    return global.System.import(entryUrl);
  }

  function unzip(buf) {
    var view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
    var out = new Map();
    var pos = 0;
    while (pos + 4 < view.byteLength) {
      if (view.getUint32(pos, true) !== 0x04034b50) break;
      var comp = view.getUint16(pos + 8, true);
      var compSize = view.getUint32(pos + 18, true);
      var nameLen = view.getUint16(pos + 26, true);
      var extraLen = view.getUint16(pos + 28, true);
      var name = new TextDecoder().decode(new Uint8Array(buf.buffer, buf.byteOffset + pos + 30, nameLen));
      var dataStart = pos + 30 + nameLen + extraLen;
      var raw = new Uint8Array(buf.buffer, buf.byteOffset + dataStart, compSize);
      var body;
      if (comp === 0) body = raw;
      else throw new Error('Unsupported zip compression method ' + comp + ' (pack with zip -0)');
      var norm = name.replace(/\\/g, '/').replace(/^\/+/, '');
      out.set(norm, body);
      pos = dataStart + compSize;
    }
    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:br，fetch 已解压成 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 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;
    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) {
          var bytes = new Uint8Array(ab);
          return normalizeBinaryPayload(bytes, isBrotli).then(function (out) {
            if (onProgress) onProgress(weightEnd);
            return out;
          });
        });
      }
      var reader = res.body.getReader();
      var chunks = [];
      var received = 0;
      var total = Number(res.headers.get('content-length')) || 0;
      function pump() {
        return reader.read().then(function (result) {
          if (result.done) {
            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);
          received += result.value.length;
          if (onProgress && total > 0) {
            var frac = received / total;
            onProgress(weightStart + (weightEnd - weightStart) * frac);
          }
          return pump();
        });
      }
      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) {
      mergeZipIntoFiles(unzip(bufs[0]));
      assertCocosRuntimeLoaded();
      mergeZipIntoFiles(unzip(bufs[1]));
      patchSettingsScriptPackages();
      assetsCoreLoaded = true;
      onProgress(0.6);
    });
  }

  function mergeZipIntoFiles(zipMap) {
    zipMap.forEach(function (v, k) { files.set(k, v); });
  }

  function patchSettingsScriptPackages() {
    var hit = files.get('src/settings.json');
    if (!hit) return;
    try {
      var settings = JSON.parse(bytesToText(hit));
      if (settings.scripting && Array.isArray(settings.scripting.scriptPackages)) {
        settings.scripting.scriptPackages = settings.scripting.scriptPackages.map(function (pkg) {
          return resolveBundleVirtualUrl(pkg) || pkg;
        });
      }
      if (settings.rendering && settings.rendering.effectSettingsPath) {
        var effectVirt = resolveBundleVirtualUrl(settings.rendering.effectSettingsPath);
        if (effectVirt) settings.rendering.effectSettingsPath = effectVirt;
      }
      files.set('src/settings.json', new TextEncoder().encode(JSON.stringify(settings)));
    } catch (e) {
      console.warn('[mstest5] patch settings failed', e);
    }
  }

  function parseCatalogBundles(catalog) {
    var ids = catalog && catalog.m_InternalIds;
    if (!ids || !ids.length) throw new Error('catalog.json missing m_InternalIds');
    var names = [];
    var i;
    for (i = 0; i < ids.length; i++) {
      var id = String(ids[i] || '');
      if (id.indexOf('.bundle') < 0) continue;
      names.push(id.split('/').pop());
    }
    if (!names.length) throw new Error('catalog.json has no .bundle entries');
    var scenes = null;
    var assets = null;
    var levels = null;
    for (i = 0; i < names.length; i++) {
      if (names[i].indexOf('scenes_all') >= 0) scenes = names[i];
      if (names[i].indexOf('assets_all') >= 0) assets = names[i];
      if (names[i].indexOf('levels_all') >= 0) levels = names[i];
    }
    if (!scenes || !assets) {
      throw new Error('catalog.json missing scenes_all/assets_all bundles: ' + names.join(', '));
    }
    if (!levels) {
      /* levels_all 已废弃，改用 levels-manifest.json 按关分包 */
    }
    return { scenes: scenes, assets: assets, levels: levels, all: names };
  }

  function loadLevelsManifest(base) {
    var manifestUrl = joinUrl(base, 'aa/levels-manifest.json');
    return origFetch(manifestUrl, { cache: 'no-cache' }).then(function (res) {
      if (!res.ok) {
        console.warn('[mstest5] 缺少 levels-manifest.json，将无法按关下载');
        return null;
      }
      return res.json();
    }).catch(function (e) {
      console.warn('[mstest5] levels-manifest.json 读取失败', e);
      return null;
    });
  }

  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');
    return origFetch(catalogUrl).then(function (res) {
      if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + catalogUrl);
      return res.json();
    }).then(function (catalog) {
      var bundles = parseCatalogBundles(catalog);
      var manifest = {
        scenesUrl: joinUrl(base, 'aa/WebGL/' + bundles.scenes),
        assetsUrl: joinUrl(base, 'aa/WebGL/' + bundles.assets),
        bundleNames: bundles,
      };
      bundleManifest = manifest;
      return manifest;
    });
  }

  function bootstrapCocos(onProgress, useLocalDisk) {
    onProgress(0.65);
    installEmbeddedPathFix();
    if (!useLocalDisk) installFetchShim();
    var canvas = document.getElementById('unity-canvas') || document.getElementById('GameCanvas');
    var load = useLocalDisk ? loadScriptSrc : loadScript;
    var mapStep = useLocalDisk ? loadImportMapFromDisk : loadImportMap;
    return load('cocos-bridge.js')
      .then(function () { onProgress(0.7); return load('src/polyfills.bundle.js'); })
      .then(function () { onProgress(0.75); return load('src/system.bundle.js'); })
      .then(function () {
        ensureSystemBase();
        onProgress(0.8);
        return mapStep();
      })
      .then(function () {
        ensureCocosDom(canvas);
        onProgress(0.85);
        return systemImportEntry(useLocalDisk);
      })
      .then(function () {
        return global.System.import('cc').then(function (cc) {
          bindCocosEngine(cc);
          return ensureCocosGameRunning(cc);
        });
      })
      .then(function () { onProgress(0.92); return waitInstance(); })
      .then(function (ins) {
        onProgress(1);
        if (!ins.SetFullscreen) {
          ins.SetFullscreen = function () {
            var el = document.getElementById('unity-container') || document.documentElement;
            if (el.requestFullscreen) el.requestFullscreen();
          };
        }
        global.unityInstance = ins;
        prefetchLevelsBundleBackground();
        var sync = global.__tfrhSyncEmbeddedCanvas;
        if (typeof sync === 'function') {
          sync();
          setTimeout(sync, 0);
          setTimeout(sync, 120);
          setTimeout(sync, 400);
          setTimeout(sync, 1000);
          setTimeout(sync, 2000);
        }
        withCocosEngine(function (cc) {
          syncEmbeddedCamerasFromLoader(cc);
        });
        return ins;
      });
  }

  // loader 解析时即安装路径修复（Cocos 后续动态 script 需要）
  packageRoot = getPackageRoot();
  installEmbeddedPathFix();

  function resolveLevelsDbBase(config) {
    var sa = config && config.streamingAssetsUrl;
    if (sa && /^https?:\/\//i.test(sa)) {
      return String(sa).replace(/\/StreamingAssets\/?$/i, '');
    }
    return packageRoot || getPackageRoot();
  }

  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);
      return res.arrayBuffer().then(function (ab) {
        return normalizeJsonPayload(ab, true).then(function (text) {
          return JSON.parse(text);
        });
      });
    }).catch(function () {
      return origFetch(url).then(function (res) {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.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] 关卡库索引预取失败，启动时将由 LevelDatabase 回退', indexUrl, e);
    });
  }

  function isRemoteCdnConfig(config) {
    var sa = config && config.streamingAssetsUrl;
    return !!(sa && /^https?:\/\//i.test(String(sa)));
  }

  function shouldUseBundlePackFlow(config) {
    if (isRemoteCdnConfig(config)) {
      return Promise.resolve(true);
    }
    var base = resolveStreamingBase(config);
    var catalogUrl = joinUrl(base, 'aa/catalog.json');
    return origFetch(catalogUrl, { cache: 'no-cache' }).then(function (res) {
      return res.ok;
    }).catch(function () {
      return false;
    });
  }

  global.createUnityInstance = function (canvas, config, onProgress) {
    onProgress = onProgress || function () {};
    onProgress(0.02);
    resolveStreamingBase(config);
    return prefetchLevelDbIndex(config).then(function () {
      return shouldUseBundlePackFlow(config);
    }).then(function (useBundles) {
      if (!useBundles) {
        return probeLocalExtracted().then(function (useLocalDisk) {
          if (useLocalDisk) {
            onProgress(0.35);
            return bootstrapCocos(onProgress, true);
          }
          return loadBundleManifest(config).then(function (manifest) {
            return loadStartupBundles(manifest, onProgress);
          }).then(function () {
            prefetchLevelsBundleBackground();
            return bootstrapCocos(onProgress, false);
          });
        });
      }
      return loadBundleManifest(config).then(function (manifest) {
        return loadStartupBundles(manifest, onProgress);
      }).then(function () {
        prefetchLevelsBundleBackground();
        return bootstrapCocos(onProgress, false);
      });
    });
  };
})(typeof window !== 'undefined' ? window : globalThis);
