(function () { if (window.__UnityCachedDownloadShimInstalled) return; window.__UnityCachedDownloadShimInstalled = true; var NativeXMLHttpRequest = window.XMLHttpRequest; var nativeFetch = window.fetch ? window.fetch.bind(window) : null; var DB_NAME = 'UnityCachedDownloads'; var DB_VERSION = 1; var STORE_NAME = 'responses'; var debugEnabled = location.search.indexOf('unityCacheDebug=1') >= 0; var debugCount = 0; var debugLimit = 120; var dbPromise = null; function log() { if (!debugEnabled || debugCount >= debugLimit || !window.console || !console.log) return; debugCount++; var args = Array.prototype.slice.call(arguments); args.unshift('[CachedXHR]'); console.log.apply(console, args); } function normalizeUrl(url) { if (!url) return ''; return String(url).split('#')[0].split('?')[0].toLowerCase(); } function getCacheConfig() { return window.UnityAssetBundleCacheConfig || {}; } function buildAbsoluteUrl(url) { try { return new URL(url, location.href); } catch (err) { return null; } } function isAllowedBundleCachePath(url) { var parsedUrl = buildAbsoluteUrl(url); if (!parsedUrl) return false; var path = parsedUrl.pathname.toLowerCase(); var config = getCacheConfig(); var allowedPathPrefixes = config.allowedPathPrefixes || []; for (var i = 0; i < allowedPathPrefixes.length; i++) { var prefix = allowedPathPrefixes[i].toLowerCase(); if (path.indexOf(prefix) === 0) return true; } return false; } function getNonBundleReason(url) { var normalizedUrl = normalizeUrl(url); if (!normalizedUrl) return 'empty-url'; if (normalizedUrl.endsWith('/filelist')) return 'filelist'; if (normalizedUrl.endsWith('/webgl')) return 'manifest'; if (normalizedUrl.indexOf('streamingassets/') >= 0 || normalizedUrl.indexOf('/streamingassets/') >= 0) return 'streaming-assets'; var nonBundleExtensions = [ '.data', '.wasm', '.js', '.json', '.css', '.html', '.htm', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.mp3', '.ogg', '.wav', '.mp4', '.webm', '.txt', '.xml', '.csv', '.br', '.gz', '.unityweb', '.mem' ]; for (var i = 0; i < nonBundleExtensions.length; i++) { if (normalizedUrl.endsWith(nonBundleExtensions[i])) return 'runtime-extension'; } return ''; } function isLikelyAssetBundleUrl(url) { var normalizedUrl = normalizeUrl(url); if (!normalizedUrl) return false; if (getNonBundleReason(url)) return false; if (!isAllowedBundleCachePath(url)) return false; if (normalizedUrl.endsWith('.bundle')) return true; return normalizedUrl.indexOf('/webgl/') >= 0 || normalizedUrl.indexOf('/assetbundles/') >= 0; } function shouldIntercept(url, method, async) { if (!nativeFetch) return false; if (async === false) return false; if (!method || String(method).toUpperCase() !== 'GET') return false; return isLikelyAssetBundleUrl(url); } function getUnityCacheControlMode(url) { var nonBundleReason = getNonBundleReason(url); if (nonBundleReason) { log('unity-cache-control', 'no-store', nonBundleReason, url); return 'no-store'; } if (isLikelyAssetBundleUrl(url)) { log('unity-cache-control', 'no-store', 'asset-bundle-shim', url); return 'no-store'; } log('unity-cache-control', 'must-revalidate', 'fallback', url); return 'must-revalidate'; } window.UnityAssetBundleCache = window.UnityAssetBundleCache || {}; window.UnityAssetBundleCache.getCacheControlMode = getUnityCacheControlMode; function openDb() { if (dbPromise) return dbPromise; dbPromise = new Promise(function (resolve, reject) { if (!window.indexedDB) { reject(new Error('IndexedDB not available')); return; } var request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = function (event) { var db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'url' }); } }; request.onsuccess = function (event) { resolve(event.target.result); }; request.onerror = function () { reject(request.error || new Error('Failed to open cache DB')); }; request.onblocked = function () { reject(new Error('Cache DB open blocked')); }; }); return dbPromise; } function readRecord(url) { return openDb().then(function (db) { return new Promise(function (resolve, reject) { var tx = db.transaction(STORE_NAME, 'readonly'); var store = tx.objectStore(STORE_NAME); var request = store.get(url); request.onsuccess = function () { resolve(request.result || null); }; request.onerror = function () { reject(request.error || new Error('Failed to read cache entry')); }; }); }); } function writeRecord(record) { return openDb().then(function (db) { return new Promise(function (resolve, reject) { var tx = db.transaction(STORE_NAME, 'readwrite'); var store = tx.objectStore(STORE_NAME); var request = store.put(record); request.onsuccess = function () { resolve(); }; request.onerror = function () { reject(request.error || new Error('Failed to write cache entry')); }; }); }); } function headersToObject(headers) { var result = {}; if (!headers || !headers.forEach) return result; headers.forEach(function (value, key) { result[key.toLowerCase()] = value; }); return result; } function headersToText(headers) { var lines = []; if (!headers) return ''; Object.keys(headers).forEach(function (key) { lines.push(key + ': ' + headers[key]); }); return lines.length ? lines.join('\r\n') + '\r\n' : ''; } function cloneArrayBuffer(buffer) { return buffer ? buffer.slice(0) : buffer; } function createEventTarget() { return { _listeners: {}, addEventListener: function (type, handler) { if (!handler) return; this._listeners[type] = this._listeners[type] || []; if (this._listeners[type].indexOf(handler) >= 0) return; this._listeners[type].push(handler); }, removeEventListener: function (type, handler) { var list = this._listeners[type]; if (!list) return; var index = list.indexOf(handler); if (index >= 0) list.splice(index, 1); }, dispatchEvent: function (event) { var list = this._listeners[event.type] || []; for (var i = 0; i < list.length; i++) { try { list[i].call(this, event); } catch (err) { setTimeout(function () { throw err; }, 0); } } } }; } function addEventListenerShim(type, handler) { if (!handler) return; this._listeners = this._listeners || {}; this._listeners[type] = this._listeners[type] || []; if (this._listeners[type].indexOf(handler) >= 0) return; this._listeners[type].push(handler); } function removeEventListenerShim(type, handler) { this._listeners = this._listeners || {}; var list = this._listeners[type]; if (!list) return; var index = list.indexOf(handler); if (index >= 0) list.splice(index, 1); } function dispatchEventShim(event) { this._listeners = this._listeners || {}; var list = this._listeners[event.type] || []; for (var i = 0; i < list.length; i++) { try { list[i].call(this, event); } catch (err) { setTimeout(function () { throw err; }, 0); } } return true; } function createShimEvent(type, init) { var event; try { event = new Event(type); } catch (e) { event = document.createEvent('Event'); event.initEvent(type, false, false); } if (init) { for (var key in init) { if (Object.prototype.hasOwnProperty.call(init, key)) { event[key] = init[key]; } } } return event; } function CachedXMLHttpRequest() { this._native = new NativeXMLHttpRequest(); this._nativeListenersBound = false; this._useCache = false; this._aborted = false; this._completed = false; this._method = 'GET'; this._url = ''; this._async = true; this._requestHeaders = {}; this._responseHeaders = {}; this._responseHeadersText = ''; this._responseText = ''; this._response = null; this._responseURL = ''; this._status = 0; this._statusText = ''; this._readyState = 0; this._responseType = ''; this._timeout = 0; this._withCredentials = false; this._overrideMimeTypeValue = null; this._onreadystatechange = null; this._onloadstart = null; this._onprogress = null; this._onload = null; this._onerror = null; this._onabort = null; this._onloadend = null; this._ontimeout = null; this._listeners = {}; this.upload = createEventTarget(); } Object.defineProperties(CachedXMLHttpRequest.prototype, { readyState: { get: function () { return this._readyState; } }, status: { get: function () { return this._status; } }, statusText: { get: function () { return this._statusText; } }, response: { get: function () { return this._response; } }, responseText: { get: function () { return this._responseText; } }, responseURL: { get: function () { return this._responseURL; } }, responseType: { get: function () { return this._responseType; }, set: function (value) { this._responseType = value || ''; if (this._native) { try { this._native.responseType = this._responseType; } catch (err) { } } } }, timeout: { get: function () { return this._timeout; }, set: function (value) { this._timeout = value || 0; if (this._native) { this._native.timeout = this._timeout; } } }, withCredentials: { get: function () { return this._withCredentials; }, set: function (value) { this._withCredentials = !!value; if (this._native) { this._native.withCredentials = this._withCredentials; } } }, onreadystatechange: { get: function () { return this._onreadystatechange; }, set: function (handler) { this._onreadystatechange = handler; } }, onloadstart: { get: function () { return this._onloadstart; }, set: function (handler) { this._onloadstart = handler; } }, onprogress: { get: function () { return this._onprogress; }, set: function (handler) { this._onprogress = handler; } }, onload: { get: function () { return this._onload; }, set: function (handler) { this._onload = handler; } }, onerror: { get: function () { return this._onerror; }, set: function (handler) { this._onerror = handler; } }, onabort: { get: function () { return this._onabort; }, set: function (handler) { this._onabort = handler; } }, onloadend: { get: function () { return this._onloadend; }, set: function (handler) { this._onloadend = handler; } }, ontimeout: { get: function () { return this._ontimeout; }, set: function (handler) { this._ontimeout = handler; } } }); CachedXMLHttpRequest.prototype._emit = function (type, init) { var event = createShimEvent(type, init); var handler = this['on' + type]; if (typeof handler === 'function') { handler.call(this, event); } if (this._listeners && this._listeners[type]) { this.dispatchEvent(event); } }; CachedXMLHttpRequest.prototype.addEventListener = addEventListenerShim; CachedXMLHttpRequest.prototype.removeEventListener = removeEventListenerShim; CachedXMLHttpRequest.prototype.dispatchEvent = dispatchEventShim; CachedXMLHttpRequest.prototype._syncFromNative = function () { if (!this._native) return; this._readyState = this._native.readyState; try { this._status = this._native.status; this._statusText = this._native.statusText || ''; } catch (err) { } try { this._responseURL = this._native.responseURL || this._url; } catch (err2) { this._responseURL = this._url; } try { this._response = this._native.response; } catch (err3) { this._response = null; } try { if (this._responseType === '' || this._responseType === 'text') { this._responseText = this._native.responseText || ''; } } catch (err4) { this._responseText = ''; } try { this._responseHeadersText = this._native.getAllResponseHeaders ? this._native.getAllResponseHeaders() : ''; } catch (err5) { this._responseHeadersText = ''; } }; CachedXMLHttpRequest.prototype._bindNativeListeners = function () { if (this._nativeListenersBound) return; this._nativeListenersBound = true; var self = this; var forward = function (type) { return function (event) { self._syncFromNative(); self._emit(type, event); }; }; this._native.onreadystatechange = forward('readystatechange'); this._native.onloadstart = forward('loadstart'); this._native.onprogress = forward('progress'); this._native.onload = forward('load'); this._native.onerror = forward('error'); this._native.onabort = forward('abort'); this._native.onloadend = forward('loadend'); this._native.ontimeout = forward('timeout'); }; CachedXMLHttpRequest.prototype.open = function (method, url, async, user, password) { this._method = method || 'GET'; this._url = url || ''; this._async = async !== false; this._aborted = false; this._completed = false; this._requestHeaders = {}; this._responseHeaders = {}; this._responseHeadersText = ''; this._responseText = ''; this._response = null; this._responseURL = ''; this._status = 0; this._statusText = ''; this._readyState = 1; this._useCache = shouldIntercept(this._url, this._method, this._async); log(this._useCache ? 'cache-open' : 'native-open', this._method, this._url); if (!this._useCache) { this._bindNativeListeners(); this._native.open(method, url, async, user, password); if (this._responseType) { try { this._native.responseType = this._responseType; } catch (err) { } } if (this._timeout) this._native.timeout = this._timeout; if (this._withCredentials) this._native.withCredentials = this._withCredentials; if (this._overrideMimeTypeValue && this._native.overrideMimeType) { try { this._native.overrideMimeType(this._overrideMimeTypeValue); } catch (err2) { } } } this._emit('readystatechange'); }; CachedXMLHttpRequest.prototype.setRequestHeader = function (name, value) { if (!this._useCache) { return this._native.setRequestHeader(name, value); } this._requestHeaders[String(name).toLowerCase()] = value; }; CachedXMLHttpRequest.prototype.overrideMimeType = function (mimeType) { this._overrideMimeTypeValue = mimeType; if (!this._useCache && this._native.overrideMimeType) { this._native.overrideMimeType(mimeType); } }; CachedXMLHttpRequest.prototype.getAllResponseHeaders = function () { if (!this._useCache) { return this._native.getAllResponseHeaders(); } return this._responseHeadersText || ''; }; CachedXMLHttpRequest.prototype.getResponseHeader = function (name) { var key = String(name || '').toLowerCase(); if (!this._useCache) { return this._native.getResponseHeader(name); } return this._responseHeaders[key] || null; }; CachedXMLHttpRequest.prototype.abort = function () { this._aborted = true; if (!this._useCache) { return this._native.abort(); } this._readyState = 0; this._emit('abort'); this._emit('loadend'); }; CachedXMLHttpRequest.prototype.send = function (body) { if (!this._useCache) { return this._native.send(body); } var self = this; this._emit('loadstart', { loaded: 0, total: 0, lengthComputable: false }); this._readyState = 2; this._emit('readystatechange'); cachedFetch(self._url, self._requestHeaders, body, self._responseType, self._withCredentials).then(function (result) { if (self._aborted) return; self._applyCachedResult(result); self._readyState = 3; self._emit('readystatechange'); self._readyState = 4; self._completed = true; self._emit('readystatechange'); self._emit('load', { loaded: result.body ? result.body.byteLength : 0, total: result.body ? result.body.byteLength : 0, lengthComputable: !!result.body }); self._emit('loadend'); }).catch(function (err) { if (self._aborted) return; log('cache-error', self._url, err && err.message ? err.message : err); self._status = 0; self._statusText = ''; self._readyState = 4; self._completed = true; self._emit('readystatechange'); self._emit('error'); self._emit('loadend'); }); }; CachedXMLHttpRequest.prototype._applyCachedResult = function (result) { this._status = result.status || 200; this._statusText = result.statusText || 'OK'; this._responseURL = result.url || this._url; this._responseHeaders = result.headers || {}; this._responseHeadersText = headersToText(this._responseHeaders); var body = result.body || null; if (this._responseType === 'arraybuffer') { this._response = body ? cloneArrayBuffer(body) : null; this._responseText = ''; } else if (this._responseType === 'blob') { this._response = body ? new Blob([cloneArrayBuffer(body)]) : null; this._responseText = ''; } else if (this._responseType === 'json') { var text = body ? new TextDecoder('utf-8').decode(body) : ''; this._responseText = text; try { this._response = text ? JSON.parse(text) : null; } catch (err) { this._response = null; } } else { this._responseText = body ? new TextDecoder('utf-8').decode(body) : ''; this._response = this._responseText; } }; function cachedFetch(url, requestHeaders, body, responseType, withCredentials) { var requestUrl = String(url); return readRecord(requestUrl).catch(function () { return null; }).then(function (cachedRecord) { var headers = new Headers(); var hasCached = !!cachedRecord; if (cachedRecord && cachedRecord.etag) { headers.set('If-None-Match', cachedRecord.etag); } if (cachedRecord && cachedRecord.lastModified) { headers.set('If-Modified-Since', cachedRecord.lastModified); } for (var key in requestHeaders) { if (Object.prototype.hasOwnProperty.call(requestHeaders, key)) { headers.set(key, requestHeaders[key]); } } var init = { method: 'GET', headers: headers, credentials: withCredentials ? 'include' : 'same-origin', cache: 'no-store', mode: 'cors' }; if (body) { init.body = body; } log(hasCached ? 'revalidate' : 'fetch', requestUrl); return nativeFetch(requestUrl, init).then(function (response) { if (response.status === 304 && cachedRecord && cachedRecord.body) { log('cache-hit-304', requestUrl); return { status: 200, statusText: 'OK', url: requestUrl, headers: cachedRecord.headers || {}, body: cloneArrayBuffer(cachedRecord.body) }; } return response.arrayBuffer().then(function (buffer) { var responseHeaders = headersToObject(response.headers); var record = { url: requestUrl, body: cloneArrayBuffer(buffer), headers: responseHeaders, etag: response.headers.get('etag') || responseHeaders['etag'] || null, lastModified: response.headers.get('last-modified') || responseHeaders['last-modified'] || null, contentType: response.headers.get('content-type') || responseHeaders['content-type'] || null, status: response.status, statusText: response.statusText, timestamp: Date.now() }; if (response.status >= 200 && response.status < 300) { writeRecord(record).catch(function (err) { log('cache-write-failed', requestUrl, err && err.message ? err.message : err); }); } return { status: response.status, statusText: response.statusText, url: response.url || requestUrl, headers: responseHeaders, body: buffer }; }); }).catch(function (err) { if (cachedRecord && cachedRecord.body) { log('network-failed-cache-fallback', requestUrl, err && err.message ? err.message : err); return { status: cachedRecord.status || 200, statusText: cachedRecord.statusText || 'OK', url: requestUrl, headers: cachedRecord.headers || {}, body: cloneArrayBuffer(cachedRecord.body) }; } throw err; }); }); } function patchFetch() { if (!nativeFetch) return; window.fetch = function (input, init) { var request = input instanceof Request ? input : new Request(input, init); if (!shouldIntercept(request.url, request.method, true)) { return nativeFetch(input, init); } var headers = {}; if (request.headers && request.headers.forEach) { request.headers.forEach(function (value, key) { headers[key.toLowerCase()] = value; }); } return cachedFetch(request.url, headers, null, request.destination, request.credentials === 'include').then(function (result) { return new Response(result.body ? result.body.slice(0) : null, { status: result.status, statusText: result.statusText, headers: result.headers }); }); }; } function patchXHR() { CachedXMLHttpRequest.UNSENT = NativeXMLHttpRequest.UNSENT; CachedXMLHttpRequest.OPENED = NativeXMLHttpRequest.OPENED; CachedXMLHttpRequest.HEADERS_RECEIVED = NativeXMLHttpRequest.HEADERS_RECEIVED; CachedXMLHttpRequest.LOADING = NativeXMLHttpRequest.LOADING; CachedXMLHttpRequest.DONE = NativeXMLHttpRequest.DONE; window.XMLHttpRequest = CachedXMLHttpRequest; } patchFetch(); patchXHR(); log('installed'); })();