/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 899: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = __webpack_require__.p + "f33720e624651f91f06b.wasm"; /***/ }), /***/ 878: /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = __webpack_require__.p + "570910818fe53bf73aa8.wasm"; /***/ }) /******/ }); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ id: moduleId, /******/ loaded: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = __webpack_modules__; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/ensure chunk */ /******/ (() => { /******/ __webpack_require__.f = {}; /******/ // This file contains only the entry chunk. /******/ // The chunk loading function for additional chunks /******/ __webpack_require__.e = (chunkId) => { /******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { /******/ __webpack_require__.f[key](chunkId, promises); /******/ return promises; /******/ }, [])); /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/get javascript chunk filename */ /******/ (() => { /******/ // This function allow to reference async chunks /******/ __webpack_require__.u = (chunkId) => { /******/ // return url for filenames based on template /******/ return "" + chunkId + ".js"; /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/global */ /******/ (() => { /******/ __webpack_require__.g = (function() { /******/ if (typeof globalThis === 'object') return globalThis; /******/ try { /******/ return this || new Function('return this')(); /******/ } catch (e) { /******/ if (typeof window === 'object') return window; /******/ } /******/ })(); /******/ })(); /******/ /******/ /* webpack/runtime/harmony module decorator */ /******/ (() => { /******/ __webpack_require__.hmd = (module) => { /******/ module = Object.create(module); /******/ if (!module.children) module.children = []; /******/ Object.defineProperty(module, 'exports', { /******/ enumerable: true, /******/ set: () => { /******/ throw new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id); /******/ } /******/ }); /******/ return module; /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/load script */ /******/ (() => { /******/ var inProgress = {}; /******/ var dataWebpackPrefix = "ruffle-extension:"; /******/ // loadScript function to load a script via script tag /******/ __webpack_require__.l = (url, done, key, chunkId) => { /******/ if(inProgress[url]) { inProgress[url].push(done); return; } /******/ var script, needAttach; /******/ if(key !== undefined) { /******/ var scripts = document.getElementsByTagName("script"); /******/ for(var i = 0; i < scripts.length; i++) { /******/ var s = scripts[i]; /******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; } /******/ } /******/ } /******/ if(!script) { /******/ needAttach = true; /******/ script = document.createElement('script'); /******/ /******/ script.charset = 'utf-8'; /******/ script.timeout = 120; /******/ if (__webpack_require__.nc) { /******/ script.setAttribute("nonce", __webpack_require__.nc); /******/ } /******/ script.setAttribute("data-webpack", dataWebpackPrefix + key); /******/ script.src = url; /******/ } /******/ inProgress[url] = [done]; /******/ var onScriptComplete = (prev, event) => { /******/ // avoid mem leaks in IE. /******/ script.onerror = script.onload = null; /******/ clearTimeout(timeout); /******/ var doneFns = inProgress[url]; /******/ delete inProgress[url]; /******/ script.parentNode && script.parentNode.removeChild(script); /******/ doneFns && doneFns.forEach((fn) => (fn(event))); /******/ if(prev) return prev(event); /******/ } /******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000); /******/ script.onerror = onScriptComplete.bind(null, script.onerror); /******/ script.onload = onScriptComplete.bind(null, script.onload); /******/ needAttach && document.head.appendChild(script); /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/publicPath */ /******/ (() => { /******/ __webpack_require__.p = ""; /******/ })(); /******/ /******/ /* webpack/runtime/jsonp chunk loading */ /******/ (() => { /******/ __webpack_require__.b = document.baseURI || self.location.href; /******/ /******/ // object to store loaded and loading chunks /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched /******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded /******/ var installedChunks = { /******/ 492: 0 /******/ }; /******/ /******/ __webpack_require__.f.j = (chunkId, promises) => { /******/ // JSONP chunk loading for javascript /******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; /******/ if(installedChunkData !== 0) { // 0 means "already installed". /******/ /******/ // a Promise means "currently loading". /******/ if(installedChunkData) { /******/ promises.push(installedChunkData[2]); /******/ } else { /******/ if(true) { // all chunks have JS /******/ // setup Promise in chunk cache /******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); /******/ promises.push(installedChunkData[2] = promise); /******/ /******/ // start chunk loading /******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId); /******/ // create error before stack unwound to get useful stacktrace later /******/ var error = new Error(); /******/ var loadingEnded = (event) => { /******/ if(__webpack_require__.o(installedChunks, chunkId)) { /******/ installedChunkData = installedChunks[chunkId]; /******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined; /******/ if(installedChunkData) { /******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type); /******/ var realSrc = event && event.target && event.target.src; /******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; /******/ error.name = 'ChunkLoadError'; /******/ error.type = errorType; /******/ error.request = realSrc; /******/ installedChunkData[1](error); /******/ } /******/ } /******/ }; /******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); /******/ } else installedChunks[chunkId] = 0; /******/ } /******/ } /******/ }; /******/ /******/ // no prefetching /******/ /******/ // no preloaded /******/ /******/ // no HMR /******/ /******/ // no HMR manifest /******/ /******/ // no on chunks loaded /******/ /******/ // install a JSONP callback for chunk loading /******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { /******/ var [chunkIds, moreModules, runtime] = data; /******/ // add "moreModules" to the modules object, /******/ // then flag all "chunkIds" as loaded and fire callback /******/ var moduleId, chunkId, i = 0; /******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) { /******/ for(moduleId in moreModules) { /******/ if(__webpack_require__.o(moreModules, moduleId)) { /******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; /******/ } /******/ } /******/ if(runtime) var result = runtime(__webpack_require__); /******/ } /******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { /******/ installedChunks[chunkId][0](); /******/ } /******/ installedChunks[chunkId] = 0; /******/ } /******/ /******/ } /******/ /******/ var chunkLoadingGlobal = self["webpackChunkruffle_extension"] = self["webpackChunkruffle_extension"] || []; /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); /******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. (() => { ;// CONCATENATED MODULE: ../core/dist/version.js /** * A representation of a semver 2 compliant version string */ class Version { /** * Construct a Version from specific components. * * If you wish to parse a string into a Version then please use [[fromSemver]]. * * @param major The major version component. * @param minor The minor version component. * @param patch The patch version component. * @param prIdent A list of pre-release identifiers, if any * @param buildIdent A list of build identifiers, if any */ constructor(major, minor, patch, prIdent, // @ts-expect-error: Property 'buildIdent' is declared but its value is never read. buildIdent) { this.major = major; this.minor = minor; this.patch = patch; this.prIdent = prIdent; this.buildIdent = buildIdent; } /** * Construct a version from a semver 2 compliant string. * * This function is intended for use with semver 2 compliant strings. * Malformed strings may still parse correctly, but this result is not * guaranteed. * * @param versionString A semver 2.0.0 compliant version string * @returns A version object */ static fromSemver(versionString) { const buildSplit = versionString.split("+"), prSplit = buildSplit[0].split("-"), versionSplit = prSplit[0].split("."); const major = parseInt(versionSplit[0], 10); let minor = 0; let patch = 0; let prIdent = null; let buildIdent = null; if (versionSplit[1] !== undefined) { minor = parseInt(versionSplit[1], 10); } if (versionSplit[2] !== undefined) { patch = parseInt(versionSplit[2], 10); } if (prSplit[1] !== undefined) { prIdent = prSplit[1].split("."); } if (buildSplit[1] !== undefined) { buildIdent = buildSplit[1].split("."); } return new Version(major, minor, patch, prIdent, buildIdent); } /** * Returns true if a given version is compatible with this one. * * Compatibility is defined as having the same nonzero major version * number, or if both major versions are zero, the same nonzero minor * version number, or if both minor versions are zero, the same nonzero * patch version number. * * This implements the ^ operator in npm's semver package, with the * exception of the prerelease exclusion rule. * * @param other The other version to test against * @returns True if compatible */ isCompatibleWith(other) { return ((this.major !== 0 && this.major === other.major) || (this.major === 0 && other.major === 0 && this.minor !== 0 && this.minor === other.minor) || (this.major === 0 && other.major === 0 && this.minor === 0 && other.minor === 0 && this.patch !== 0 && this.patch === other.patch)); } /** * Returns true if this version has precedence over (is newer than) another * version. * * Precedence is defined as in the Semver 2 spec. This implements the > * operator in npm's semver package, with the exception of the prerelease * exclusion rule. * * @param other The other version to test against * @returns True if this version has precedence over the other one */ hasPrecedenceOver(other) { if (this.major > other.major) { return true; } else if (this.major < other.major) { return false; } if (this.minor > other.minor) { return true; } else if (this.minor < other.minor) { return false; } if (this.patch > other.patch) { return true; } else if (this.patch < other.patch) { return false; } if (this.prIdent === null && other.prIdent !== null) { return true; } else if (this.prIdent !== null && other.prIdent !== null) { const isNumeric = /^[0-9]*$/; for (let i = 0; i < this.prIdent.length && i < other.prIdent.length; i += 1) { if (!isNumeric.test(this.prIdent[i]) && isNumeric.test(other.prIdent[i])) { return true; } else if (isNumeric.test(this.prIdent[i]) && isNumeric.test(other.prIdent[i])) { if (parseInt(this.prIdent[i], 10) > parseInt(other.prIdent[i], 10)) { return true; } else if (parseInt(this.prIdent[i], 10) < parseInt(other.prIdent[i], 10)) { return false; } } else if (isNumeric.test(this.prIdent[i]) && !isNumeric.test(other.prIdent[i])) { return false; } else if (!isNumeric.test(this.prIdent[i]) && !isNumeric.test(other.prIdent[i])) { if (this.prIdent[i] > other.prIdent[i]) { return true; } else if (this.prIdent[i] < other.prIdent[i]) { return false; } } } return this.prIdent.length > other.prIdent.length; } return false; } /** * Tests if a given version is equivalent to this one. * * Build and prerelease tags are ignored. * * @param other The other version to test against * @returns True if the given version is equivalent */ isEqual(other) { return (this.major === other.major && this.minor === other.minor && this.patch === other.patch); } /** * Tests if a given version is stable or a compatible prerelease for this * version. * * This implements the prerelease exclusion rule of NPM semver: a * prerelease version can only pass this check if the major/minor/patch * components of both versions are the same. Otherwise, the prerelease * version always fails. * * @param other The other version to test against * @returns True if the given version is either stable, or a * prerelease in the same series as this one. */ isStableOrCompatiblePrerelease(other) { if (other.prIdent === null) { return true; } else { return (this.major === other.major && this.minor === other.minor && this.patch === other.patch); } } } ;// CONCATENATED MODULE: ../core/dist/version-range.js /** * Represents a set of version requirements. */ class VersionRange { /** * Constructs a range of versions as specified by the given requirements. * * If you wish to construct this object from a string representation, * then use [[fromRequirementString]]. * * @param requirements Requirements to set this range by */ constructor(requirements) { this.requirements = requirements; } /** * Determine if a given version satisfies this range. * * @param fver A version object to test against. * @returns Whether or not the given version matches this range */ satisfiedBy(fver) { for (const requirement of this.requirements) { let matches = true; for (const { comparator, version } of requirement) { matches = matches && version.isStableOrCompatiblePrerelease(fver); if (comparator === "" || comparator === "=") { matches = matches && version.isEqual(fver); } else if (comparator === ">") { matches = matches && fver.hasPrecedenceOver(version); } else if (comparator === ">=") { matches = matches && (fver.hasPrecedenceOver(version) || version.isEqual(fver)); } else if (comparator === "<") { matches = matches && version.hasPrecedenceOver(fver); } else if (comparator === "<=") { matches = matches && (version.hasPrecedenceOver(fver) || version.isEqual(fver)); } else if (comparator === "^") { matches = matches && version.isCompatibleWith(fver); } } if (matches) { return true; } } return false; } /** * Parse a requirement string into a version range. * * @param requirement The version requirements, consisting of a * series of space-separated strings, each one being a semver version * optionally prefixed by a comparator or a separator. * * Valid comparators are: * - `""` or `"="`: Precisely this version * - `">`": A version newer than this one * - `">`=": A version newer or equal to this one * - `"<"`: A version older than this one * - `"<="`: A version older or equal to this one * - `"^"`: A version that is compatible with this one * * A separator is `"||`" which splits the requirement string into * left OR right. * @returns A version range object. */ static fromRequirementString(requirement) { const components = requirement.split(" "); let set = []; const requirements = []; for (const component of components) { if (component === "||") { if (set.length > 0) { requirements.push(set); set = []; } } else if (component.length > 0) { const match = /[0-9]/.exec(component); if (match) { const comparator = component.slice(0, match.index).trim(); const version = Version.fromSemver(component.slice(match.index).trim()); set.push({ comparator, version }); } } } if (set.length > 0) { requirements.push(set); } return new VersionRange(requirements); } } ;// CONCATENATED MODULE: ../../node_modules/wasm-feature-detect/dist/esm/index.js const bigInt=()=>(async e=>{try{return(await WebAssembly.instantiate(e)).instance.exports.b(BigInt(0))===BigInt(0)}catch(e){return!1}})(new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,1,126,1,126,3,2,1,0,7,5,1,1,98,0,0,10,6,1,4,0,32,0,11])),bulkMemory=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),exceptions=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,6,64,25,11,11])),extendedConst=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,5,3,1,0,1,11,9,1,0,65,1,65,2,106,11,0])),gc=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,10,2,95,1,125,0,96,0,1,107,0,3,2,1,1,10,12,1,10,0,67,0,0,0,0,251,7,0,11])),memory64=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,5,3,1,4,1])),multiValue=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,0,2,127,127,3,2,1,0,10,8,1,6,0,65,0,65,0,11])),mutableGlobals=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,2,8,1,1,97,1,98,3,127,1,6,6,1,127,1,65,0,11,7,5,1,1,97,3,1])),referenceTypes=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,7,1,5,0,208,112,26,11])),relaxedSimd=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),saturatedFloatToInt=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,12,1,10,0,67,0,0,0,0,252,0,26,11])),signExtensions=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,65,0,192,26,11])),simd=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),streamingCompilation=()=>(async()=>"compileStreaming"in WebAssembly)(),tailCall=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),threads=()=>(async e=>{try{return"undefined"!=typeof MessageChannel&&(new MessageChannel).port1.postMessage(new SharedArrayBuffer(1)),WebAssembly.validate(e)}catch(e){return!1}})(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,4,1,3,1,1,10,11,1,9,0,65,0,254,16,2,0,26,11])); ;// CONCATENATED MODULE: ../core/dist/js-polyfills.js /** * Polyfills the `Array.prototype.reduce` method. * * Production steps of ECMA-262, Edition 5, 15.4.4.21 * Reference: https://es5.github.io/#x15.4.4.21 * https://tc39.github.io/ecma262/#sec-array.prototype.reduce */ function polyfillArrayPrototypeReduce() { Object.defineProperty(Array.prototype, "reduce", { value(...args) { if (args.length === 0 && window.Prototype && window.Prototype.Version && window.Prototype.Version < "1.6.1") { // Off-spec: compatibility with prototype.js return this.length > 1 ? this : this[0]; } const callback = args[0]; if (this === null) { throw new TypeError("Array.prototype.reduce called on null or undefined"); } if (typeof callback !== "function") { throw new TypeError(`${callback} is not a function`); } const o = Object(this); const len = o.length >>> 0; let k = 0; let value; if (args.length >= 2) { value = args[1]; } else { while (k < len && !(k in o)) { k++; } if (k >= len) { throw new TypeError("Reduce of empty array with no initial value"); } value = o[k++]; } while (k < len) { if (k in o) { value = callback(value, o[k], k, o); } k++; } return value; }, }); } /** * Polyfills the `Window` function. */ function polyfillWindow() { if (typeof window.constructor !== "function" || !isNativeFunction(window.constructor)) { // Don't polyfill `Window` if `window.constructor` has been overridden. return; } // @ts-expect-error: `Function not assignable to { new (): Window; prototype: Window; }` window.Window = window.constructor; } /** * Polyfills the `Reflect` object and members. * * This is a partial implementation, just enough to match our needs. */ function tryPolyfillReflect() { if (window.Reflect === undefined || window.Reflect === null) { // @ts-expect-error: {} indeed doesn't implement Reflect's interface. window.Reflect = {}; } if (typeof Reflect.get !== "function") { Object.defineProperty(Reflect, "get", { value(target, key) { return target[key]; }, }); } if (typeof Reflect.set !== "function") { Object.defineProperty(Reflect, "set", { value(target, key, value) { target[key] = value; }, }); } if (typeof Reflect.has !== "function") { Object.defineProperty(Reflect, "has", { value(target, key) { // @ts-expect-error: Type 'T' is not assignable to type 'object'. return key in target; }, }); } if (typeof Reflect.ownKeys !== "function") { Object.defineProperty(Reflect, "ownKeys", { value(target) { return [ ...Object.getOwnPropertyNames(target), ...Object.getOwnPropertySymbols(target), ]; }, }); } } /** * Determines whether a function is native or not. * * @param func The function to test. * @returns True if the function hasn't been overridden. */ // eslint-disable-next-line @typescript-eslint/ban-types function isNativeFunction(func) { const val = typeof Function.prototype.toString === "function" ? Function.prototype.toString() : null; if (typeof val === "string" && val.indexOf("[native code]") >= 0) { return (Function.prototype.toString.call(func).indexOf("[native code]") >= 0); } return false; } /** * Checks and applies the polyfills to the current window, if needed. */ function setPolyfillsOnLoad() { if (typeof Array.prototype.reduce !== "function" || !isNativeFunction(Array.prototype.reduce)) { // Some external libraries override the `Array.prototype.reduce` method in a way // that causes Webpack to crash (#1507, #1865), so we need to override it again. polyfillArrayPrototypeReduce(); } if (typeof Window !== "function" || !isNativeFunction(Window)) { // Overriding the native `Window` function causes issues in wasm-bindgen, as a // code like `window instanceof Window` will no longer work. polyfillWindow(); } // Some pages override the native `Reflect` object, which causes various issues: // 1- wasm-bindgen's stdlib may crash (#3173). // 2- FlashVars may be ignored (#8537). tryPolyfillReflect(); } ;// CONCATENATED MODULE: ../core/dist/public-path.js // This must be in global scope because `document.currentScript` // works only while the script is initially being processed. let currentScriptURL = ""; try { if (document.currentScript !== undefined && document.currentScript !== null && "src" in document.currentScript && document.currentScript.src !== "") { let src = document.currentScript.src; // CDNs allow omitting the filename. If it's omitted, append a slash to // prevent the last component from being dropped. if (!src.endsWith(".js") && !src.endsWith("/")) { src += "/"; } currentScriptURL = new URL(".", src).href; } } catch (e) { console.warn("Unable to get currentScript URL"); } /** * Attempt to discover the public path of the current Ruffle source. This can * be used to configure Webpack. * * A global public path can be specified for all sources using the RufflePlayer * config: * * ```js * window.RufflePlayer.config.publicPath = "/dist/"; * ``` * * If no such config is specified, then the parent path of where this script is * hosted is assumed, which should be the correct default in most cases. * * @param config The `window.RufflePlayer.config` object. * @returns The public path for the given source. */ function publicPath(config) { // Default to the directory where this script resides. let path = currentScriptURL; if ("publicPath" in config && config.publicPath !== null && config.publicPath !== undefined) { path = config.publicPath; } // Webpack expects the paths to end with a slash. if (path !== "" && !path.endsWith("/")) { path += "/"; } return path; } ;// CONCATENATED MODULE: ../core/dist/load-ruffle.js /** * Conditional ruffle loader */ /** * Load ruffle from an automatically-detected location. * * This function returns a new instance of Ruffle and downloads it every time. * You should not use it directly; this module will memoize the resource * download. * * @param config The `window.RufflePlayer.config` object. * @param progressCallback The callback that will be run with Ruffle's download progress. * @returns A ruffle constructor that may be used to create new Ruffle * instances. */ async function fetchRuffle(config, progressCallback) { // Apply some pure JavaScript polyfills to prevent conflicts with external // libraries, if needed. setPolyfillsOnLoad(); // NOTE: Keep this list in sync with $RUSTFLAGS in the CI build config! const extensionsSupported = (await Promise.all([ bulkMemory(), simd(), saturatedFloatToInt(), signExtensions(), referenceTypes(), ])).every(Boolean); if (!extensionsSupported) { console.log("Some WebAssembly extensions are NOT available, falling back to the vanilla WebAssembly module"); } __webpack_require__.p = publicPath(config); // Note: The argument passed to import() has to be a simple string literal, // otherwise some bundler will get confused and won't include the module? const { default: init, Ruffle } = await (extensionsSupported ? __webpack_require__.e(/* import() */ 339).then(__webpack_require__.bind(__webpack_require__, 339)) : __webpack_require__.e(/* import() */ 159).then(__webpack_require__.bind(__webpack_require__, 159))); let response; const wasmUrl = extensionsSupported ? new URL(/* asset import */ __webpack_require__(899), __webpack_require__.b) : new URL(/* asset import */ __webpack_require__(878), __webpack_require__.b); const wasmResponse = await fetch(wasmUrl); if (progressCallback) { const contentLength = wasmResponse.headers.get("content-length") || ""; let bytesLoaded = 0; // Use parseInt rather than Number so the empty string is coerced to NaN instead of 0 const bytesTotal = parseInt(contentLength); response = new Response(new ReadableStream({ async start(controller) { var _a; const reader = (_a = wasmResponse.body) === null || _a === void 0 ? void 0 : _a.getReader(); if (!reader) { throw "Response had no body"; } progressCallback(bytesLoaded, bytesTotal); for (;;) { const { done, value } = await reader.read(); if (done) { break; } if (value === null || value === void 0 ? void 0 : value.byteLength) { bytesLoaded += value === null || value === void 0 ? void 0 : value.byteLength; } controller.enqueue(value); progressCallback(bytesLoaded, bytesTotal); } controller.close(); }, }), wasmResponse); } else { response = wasmResponse; } await init(response); return Ruffle; } let lastLoaded = null; /** * Obtain an instance of `Ruffle`. * * This function returns a promise which yields `Ruffle` asynchronously. * * @param config The `window.RufflePlayer.config` object. * @param progressCallback The callback that will be run with Ruffle's download progress. * @returns A ruffle constructor that may be used to create new Ruffle * instances. */ function loadRuffle(config, progressCallback) { if (lastLoaded === null) { lastLoaded = fetchRuffle(config, progressCallback); } return lastLoaded; } ;// CONCATENATED MODULE: ../core/dist/shadow-template.js /** * The shadow template which is used to fill the actual Ruffle player element * on the page. */ const ruffleShadowTemplate = document.createElement("template"); ruffleShadowTemplate.innerHTML = `
Click to unmute
`; ;// CONCATENATED MODULE: ../core/dist/register-element.js /** * Number of times to try defining a custom element. */ const MAX_TRIES = 999; /** * A mapping between internal element IDs and DOM element IDs. */ const privateRegistry = {}; /** * Lookup a previously registered custom element. * * The returned object will have `name`, `class`, and `internal_name` * properties listing the external name, implementing class, and internal name * respectively. * * @param elementName The internal element name, previously used to * register the element with the private registry. * @returns The element data in the registry, or null if there is * no such element name registered. */ function lookupElement(elementName) { const data = privateRegistry[elementName]; if (data !== undefined) { return { internalName: elementName, name: data.name, class: data.class, }; } else { return null; } } /** * Register a custom element. * * This function is designed to be tolerant of naming conflicts. If * registration fails, we modify the name, and try again. As a result, this * function returns the real element name to use. * * Calling this function multiple times will *not* register multiple elements. * We store a private registry mapping internal element names to DOM names. * Thus, the proper way to use this function is to call it every time you are * about to work with custom element names. * * @param elementName The internal name of the element. * @param elementClass The class of the element. * * You must call this function with the same class every time. * @returns The actual element name. * @throws Throws an error if two different elements were registered with the * same internal name. */ function registerElement(elementName, elementClass) { const registration = privateRegistry[elementName]; if (registration !== undefined) { if (registration.class !== elementClass) { throw new Error("Internal naming conflict on " + elementName); } else { return registration.name; } } let tries = 0; if (window.customElements !== undefined) { while (tries < MAX_TRIES) { let externalName = elementName; if (tries > 0) { externalName = externalName + "-" + tries; } if (window.customElements.get(externalName) !== undefined) { tries += 1; continue; } else { window.customElements.define(externalName, elementClass); } privateRegistry[elementName] = { class: elementClass, name: externalName, internalName: elementName, }; return externalName; } } throw new Error("Failed to assign custom element " + elementName); } ;// CONCATENATED MODULE: ../core/dist/config.js const DEFAULT_CONFIG = { allowScriptAccess: false, parameters: {}, autoplay: "auto" /* AutoPlay.Auto */, backgroundColor: null, letterbox: "fullscreen" /* Letterbox.Fullscreen */, unmuteOverlay: "visible" /* UnmuteOverlay.Visible */, upgradeToHttps: true, compatibilityRules: true, warnOnUnsupportedContent: true, logLevel: "error" /* LogLevel.Error */, showSwfDownload: false, contextMenu: true, // Backwards-compatibility option preloader: true, splashScreen: true, maxExecutionDuration: 15, base: null, menu: true, salign: "", quality: "high", scale: "showAll", forceScale: false, frameRate: null, wmode: "opaque" /* WindowMode.Opaque */, publicPath: null, polyfills: true, playerVersion: null, preferredRenderer: null, }; ;// CONCATENATED MODULE: ../core/dist/flash-identifiers.js const FLASH_MIMETYPE = "application/x-shockwave-flash"; const FUTURESPLASH_MIMETYPE = "application/futuresplash"; const FLASH7_AND_8_MIMETYPE = "application/x-shockwave-flash2-preview"; const FLASH_MOVIE_MIMETYPE = "application/vnd.adobe.flash.movie"; const FLASH_ACTIVEX_CLASSID = "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; ;// CONCATENATED MODULE: ../core/dist/swf-utils.js /** * Returns whether the given filename ends in a known flash extension. * * @param filename The filename to test. * @returns True if the filename is a flash movie (swf or spl). */ function isSwfFilename(filename) { if (filename) { let pathname = ""; try { // A base URL is required if `filename` is a relative URL, but we don't need to detect the real URL origin. pathname = new URL(filename, "https://example.com").pathname; } catch (err) { // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation. } if (pathname && pathname.length >= 4) { const extension = pathname.slice(-4).toLowerCase(); if (extension === ".swf" || extension === ".spl") { return true; } } } return false; } /** * Returns whether the given MIME type is a known flash type. * * @param mimeType The MIME type to test. * @returns True if the MIME type is a flash MIME type. */ function isSwfMimeType(mimeType) { switch (mimeType.toLowerCase()) { case FLASH_MIMETYPE.toLowerCase(): case FUTURESPLASH_MIMETYPE.toLowerCase(): case FLASH7_AND_8_MIMETYPE.toLowerCase(): case FLASH_MOVIE_MIMETYPE.toLowerCase(): return true; default: return false; } } /** * Create a filename to save a downloaded SWF into. * * @param swfUrl The URL of the SWF file. * @returns The filename the SWF file can be saved at. */ function swfFileName(swfUrl) { const pathName = swfUrl.pathname; const name = pathName.substring(pathName.lastIndexOf("/") + 1); return name; } ;// CONCATENATED MODULE: ../core/dist/build-info.js /** * Stores build information. The string literals are replaces at compile time by `set_version.js`. */ const buildInfo = { versionNumber: "0.1.0", versionName: "nightly 2023-04-29", versionChannel: "nightly", buildDate: "2023-04-29T00:20:24.499Z", commitHash: "1f956ffe55525657dc4748ce39861a995c2fe2a9", }; ;// CONCATENATED MODULE: ../core/dist/ruffle-player.js const RUFFLE_ORIGIN = "https://ruffle.rs"; const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/; let isAudioContextUnmuted = false; /** * Converts arbitrary input to an easy to use record object. * * @param parameters Parameters to sanitize * @returns A sanitized map of param name to param value */ function sanitizeParameters(parameters) { if (parameters === null || parameters === undefined) { return {}; } if (!(parameters instanceof URLSearchParams)) { parameters = new URLSearchParams(parameters); } const output = {}; for (const [key, value] of parameters) { // Every value must be type of string output[key] = value.toString(); } return output; } class Point { constructor(x, y) { this.x = x; this.y = y; } distanceTo(other) { const dx = other.x - this.x; const dy = other.y - this.y; return Math.sqrt(dx * dx + dy * dy); } } /** * The ruffle player element that should be inserted onto the page. * * This element will represent the rendered and intractable flash movie. */ class RufflePlayer extends HTMLElement { /** * Indicates the readiness of the playing movie. * * @returns The `ReadyState` of the player. */ get readyState() { return this._readyState; } /** * The metadata of the playing movie (such as movie width and height). * These are inherent properties stored in the SWF file and are not affected by runtime changes. * For example, `metadata.width` is the width of the SWF file, and not the width of the Ruffle player. * * @returns The metadata of the movie, or `null` if the movie metadata has not yet loaded. */ get metadata() { return this._metadata; } /** * Constructs a new Ruffle flash player for insertion onto the page. */ constructor() { super(); // Allows the user to permanently disable the context menu. this.contextMenuForceDisabled = false; // Whether this device is a touch device. // Set to true when a touch event is encountered. this.isTouch = false; // Whether this device sends contextmenu events. // Set to true when a contextmenu event is seen. this.contextMenuSupported = false; this.panicked = false; this._cachedDebugInfo = null; this.isExtension = false; this.longPressTimer = null; this.pointerDownPosition = null; this.pointerMoveMaxDistance = 0; /** * Any configuration that should apply to this specific player. * This will be defaulted with any global configuration. */ this.config = {}; this.shadow = this.attachShadow({ mode: "open" }); this.shadow.appendChild(ruffleShadowTemplate.content.cloneNode(true)); this.dynamicStyles = (this.shadow.getElementById("dynamic_styles")); this.container = this.shadow.getElementById("container"); this.playButton = this.shadow.getElementById("play_button"); this.playButton.addEventListener("click", () => this.play()); this.unmuteOverlay = this.shadow.getElementById("unmute_overlay"); this.splashScreen = this.shadow.getElementById("splash-screen"); this.virtualKeyboard = (this.shadow.getElementById("virtual-keyboard")); this.virtualKeyboard.addEventListener("input", this.virtualKeyboardInput.bind(this)); this.saveManager = (this.shadow.getElementById("save-manager")); this.saveManager.addEventListener("click", () => this.saveManager.classList.add("hidden")); const modalArea = this.saveManager.querySelector("#modal-area"); if (modalArea) { modalArea.addEventListener("click", (event) => event.stopPropagation()); } const closeSaveManager = this.saveManager.querySelector("#close-modal"); if (closeSaveManager) { closeSaveManager.addEventListener("click", () => this.saveManager.classList.add("hidden")); } const backupSaves = this.saveManager.querySelector("#backup-saves"); if (backupSaves) { backupSaves.addEventListener("click", this.backupSaves.bind(this)); } this.contextMenuElement = this.shadow.getElementById("context-menu"); window.addEventListener("pointerdown", this.checkIfTouch.bind(this)); this.addEventListener("contextmenu", this.showContextMenu.bind(this)); this.container.addEventListener("pointerdown", this.pointerDown.bind(this)); this.container.addEventListener("pointermove", this.checkLongPressMovement.bind(this)); this.container.addEventListener("pointerup", this.checkLongPress.bind(this)); this.container.addEventListener("pointercancel", this.clearLongPressTimer.bind(this)); this.addEventListener("fullscreenchange", this.fullScreenChange.bind(this)); this.addEventListener("webkitfullscreenchange", this.fullScreenChange.bind(this)); this.instance = null; this.onFSCommand = null; this._readyState = 0 /* ReadyState.HaveNothing */; this._metadata = null; this.lastActivePlayingState = false; this.setupPauseOnTabHidden(); } /** * Setup event listener to detect when tab is not active to pause instance playback. * this.instance.play() is called when the tab becomes visible only if the * the instance was not paused before tab became hidden. * * See: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API * @ignore * @internal */ setupPauseOnTabHidden() { document.addEventListener("visibilitychange", () => { if (!this.instance) { return; } // Tab just changed to be inactive. Record whether instance was playing. if (document.hidden) { this.lastActivePlayingState = this.instance.is_playing(); this.instance.pause(); } // Play only if instance was playing originally. if (!document.hidden && this.lastActivePlayingState === true) { this.instance.play(); } }, false); } /** * @ignore * @internal */ connectedCallback() { this.updateStyles(); } /** * @ignore * @internal */ static get observedAttributes() { return ["width", "height"]; } /** * @ignore * @internal */ attributeChangedCallback(name, _oldValue, _newValue) { if (name === "width" || name === "height") { this.updateStyles(); } } /** * @ignore * @internal */ disconnectedCallback() { this.destroy(); } /** * Updates the internal shadow DOM to reflect any set attributes from * this element. */ updateStyles() { if (this.dynamicStyles.sheet) { if (this.dynamicStyles.sheet.rules) { for (let i = 0; i < this.dynamicStyles.sheet.rules.length; i++) { this.dynamicStyles.sheet.deleteRule(i); } } const widthAttr = this.attributes.getNamedItem("width"); if (widthAttr !== undefined && widthAttr !== null) { const width = RufflePlayer.htmlDimensionToCssDimension(widthAttr.value); if (width !== null) { this.dynamicStyles.sheet.insertRule(`:host { width: ${width}; }`); } } const heightAttr = this.attributes.getNamedItem("height"); if (heightAttr !== undefined && heightAttr !== null) { const height = RufflePlayer.htmlDimensionToCssDimension(heightAttr.value); if (height !== null) { this.dynamicStyles.sheet.insertRule(`:host { height: ${height}; }`); } } } } /** * Determine if this element is the fallback content of another Ruffle * player. * * This heuristic assumes Ruffle objects will never use their fallback * content. If this changes, then this code also needs to change. * * @private */ isUnusedFallbackObject() { const element = lookupElement("ruffle-object"); if (element !== null) { let parent = this.parentNode; while (parent !== document && parent !== null) { if (parent.nodeName === element.name) { return true; } parent = parent.parentNode; } } return false; } /** * Ensure a fresh Ruffle instance is ready on this player before continuing. * * @throws Any exceptions generated by loading Ruffle Core will be logged * and passed on. * * @private */ async ensureFreshInstance() { var _a; this.destroy(); if (this.loadedConfig && this.loadedConfig.splashScreen !== false && this.loadedConfig.preloader !== false) { this.showSplashScreen(); } if (this.loadedConfig && this.loadedConfig.preloader === false) { console.warn("The configuration option preloader has been replaced with splashScreen. If you own this website, please update the configuration."); } if (this.loadedConfig && this.loadedConfig.maxExecutionDuration && typeof this.loadedConfig.maxExecutionDuration !== "number") { console.warn("Configuration: An obsolete format for duration for 'maxExecutionDuration' was used, " + "please use a single number indicating seconds instead. For instance '15' instead of " + "'{secs: 15, nanos: 0}'."); } const ruffleConstructor = await loadRuffle(this.loadedConfig || {}, this.onRuffleDownloadProgress.bind(this)).catch((e) => { console.error(`Serious error loading Ruffle: ${e}`); // Serious duck typing. In error conditions, let's not make assumptions. if (window.location.protocol === "file:") { e.ruffleIndexError = 2 /* PanicError.FileProtocol */; } else { e.ruffleIndexError = 9 /* PanicError.WasmNotFound */; const message = String(e.message).toLowerCase(); if (message.includes("mime")) { e.ruffleIndexError = 8 /* PanicError.WasmMimeType */; } else if (message.includes("networkerror") || message.includes("failed to fetch")) { e.ruffleIndexError = 6 /* PanicError.WasmCors */; } else if (message.includes("disallowed by embedder")) { e.ruffleIndexError = 1 /* PanicError.CSPConflict */; } else if (e.name === "CompileError") { e.ruffleIndexError = 3 /* PanicError.InvalidWasm */; } else if (message.includes("could not download wasm module") && e.name === "TypeError") { e.ruffleIndexError = 7 /* PanicError.WasmDownload */; } else if (e.name === "TypeError") { e.ruffleIndexError = 5 /* PanicError.JavascriptConflict */; } else if (navigator.userAgent.includes("Edg") && message.includes("webassembly is not defined")) { // Microsoft Edge detection. e.ruffleIndexError = 10 /* PanicError.WasmDisabledMicrosoftEdge */; } } this.panic(e); throw e; }); this.instance = await new ruffleConstructor(this.container, this, this.loadedConfig); this._cachedDebugInfo = this.instance.renderer_debug_info(); console.log("New Ruffle instance created (WebAssembly extensions: " + (ruffleConstructor.is_wasm_simd_used() ? "ON" : "OFF") + ")"); // In Firefox, AudioContext.state is always "suspended" when the object has just been created. // It may change by itself to "running" some milliseconds later. So we need to wait a little // bit before checking if autoplay is supported and applying the instance config. if (this.audioState() !== "running") { this.container.style.visibility = "hidden"; await new Promise((resolve) => { window.setTimeout(() => { resolve(); }, 200); }); this.container.style.visibility = ""; } this.unmuteAudioContext(); // On Android, the virtual keyboard needs to be dismissed as otherwise it re-focuses when clicking elsewhere if (navigator.userAgent.toLowerCase().includes("android")) { this.container.addEventListener("click", () => this.virtualKeyboard.blur()); } // Treat invalid values as `AutoPlay.Auto`. if (!this.loadedConfig || this.loadedConfig.autoplay === "on" /* AutoPlay.On */ || (this.loadedConfig.autoplay !== "off" /* AutoPlay.Off */ && this.audioState() === "running")) { this.play(); if (this.audioState() !== "running") { // Treat invalid values as `UnmuteOverlay.Visible`. if (!this.loadedConfig || this.loadedConfig.unmuteOverlay !== "hidden" /* UnmuteOverlay.Hidden */) { this.unmuteOverlay.style.display = "block"; } this.container.addEventListener("click", this.unmuteOverlayClicked.bind(this), { once: true, }); const audioContext = (_a = this.instance) === null || _a === void 0 ? void 0 : _a.audio_context(); if (audioContext) { audioContext.onstatechange = () => { if (audioContext.state === "running") { this.unmuteOverlayClicked(); } audioContext.onstatechange = null; }; } } } else { this.playButton.style.display = "block"; } } /** * Uploads the splash screen progress bar. * * @param bytesLoaded The size of the Ruffle WebAssembly file downloaded so far. * @param bytesTotal The total size of the Ruffle WebAssembly file. */ onRuffleDownloadProgress(bytesLoaded, bytesTotal) { const loadBar = (this.splashScreen.querySelector(".loadbar-inner")); const outerLoadbar = (this.splashScreen.querySelector(".loadbar")); if (Number.isNaN(bytesTotal)) { if (outerLoadbar) { outerLoadbar.style.display = "none"; } } else { loadBar.style.width = `${100.0 * (bytesLoaded / bytesTotal)}%`; } } /** * Destroys the currently running instance of Ruffle. */ destroy() { if (this.instance) { this.instance.destroy(); this.instance = null; this._metadata = null; this._readyState = 0 /* ReadyState.HaveNothing */; console.log("Ruffle instance destroyed."); } } checkOptions(options) { if (typeof options === "string") { return { url: options }; } const check = (condition, message) => { if (!condition) { const error = new TypeError(message); error.ruffleIndexError = 4 /* PanicError.JavascriptConfiguration */; this.panic(error); throw error; } }; check(options !== null && typeof options === "object", "Argument 0 must be a string or object"); check("url" in options || "data" in options, "Argument 0 must contain a `url` or `data` key"); check(!("url" in options) || typeof options.url === "string", "`url` must be a string"); return options; } /** * Gets the configuration set by the Ruffle extension * * @returns The configuration set by the Ruffle extension */ getExtensionConfig() { var _a; return window.RufflePlayer && window.RufflePlayer.conflict && (window.RufflePlayer.conflict["newestName"] === "extension" || window.RufflePlayer["newestName"] === "extension") ? (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.conflict["config"] : {}; } /** * Loads a specified movie into this player. * * This will replace any existing movie that may be playing. * * @param options One of the following: * - A URL, passed as a string, which will load a URL with default options. * - A [[URLLoadOptions]] object, to load a URL with options. * - A [[DataLoadOptions]] object, to load data with options. * * The options will be defaulted by the [[config]] field, which itself * is defaulted by a global `window.RufflePlayer.config`. */ async load(options) { var _a, _b; options = this.checkOptions(options); if (!this.isConnected || this.isUnusedFallbackObject()) { console.warn("Ignoring attempt to play a disconnected or suspended Ruffle element"); return; } if (isFallbackElement(this)) { // Silently fail on attempt to play a Ruffle element inside a specific node. return; } try { const extensionConfig = this.getExtensionConfig(); this.loadedConfig = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, DEFAULT_CONFIG), extensionConfig), ((_b = (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {})), this.config), options); // `allowScriptAccess` can only be set in `options`. this.loadedConfig.allowScriptAccess = options.allowScriptAccess === true; // Pre-emptively set background color of container while Ruffle/SWF loads. if (this.loadedConfig.backgroundColor && this.loadedConfig.wmode !== "transparent" /* WindowMode.Transparent */) { this.container.style.backgroundColor = this.loadedConfig.backgroundColor; } await this.ensureFreshInstance(); if ("url" in options) { console.log(`Loading SWF file ${options.url}`); this.swfUrl = new URL(options.url, document.baseURI); const parameters = Object.assign(Object.assign({}, sanitizeParameters(options.url.substring(options.url.indexOf("?")))), sanitizeParameters(options.parameters)); this.instance.stream_from(this.swfUrl.href, parameters); } else if ("data" in options) { console.log("Loading SWF data"); this.instance.load_data(new Uint8Array(options.data), sanitizeParameters(options.parameters), options.swfFileName || "movie.swf"); } } catch (e) { console.error(`Serious error occurred loading SWF file: ${e}`); const err = new Error(e); if (err.message.includes("Error parsing config")) { err.ruffleIndexError = 4 /* PanicError.JavascriptConfiguration */; } this.panic(err); throw err; } } /** * Plays or resumes the movie. */ play() { if (this.instance) { this.instance.play(); this.playButton.style.display = "none"; } } /** * Whether this player is currently playing. * * @returns True if this player is playing, false if it's paused or hasn't started yet. */ get isPlaying() { if (this.instance) { return this.instance.is_playing(); } return false; } /** * Returns the master volume of the player. * * @returns The volume. 1.0 is 100% volume. */ get volume() { if (this.instance) { return this.instance.volume(); } return 1.0; } /** * Sets the master volume of the player. * * @param value The volume. 1.0 is 100% volume. */ set volume(value) { if (this.instance) { this.instance.set_volume(value); } } /** * Checks if this player is allowed to be fullscreen by the browser. * * @returns True if you may call [[enterFullscreen]]. */ get fullscreenEnabled() { return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled); } /** * Checks if this player is currently fullscreen inside the browser. * * @returns True if it is fullscreen. */ get isFullscreen() { return ((document.fullscreenElement || document.webkitFullscreenElement) === this); } /** * Exported function that requests the browser to change the fullscreen state if * it is allowed. * * @param isFull Whether to set to fullscreen or return to normal. */ setFullscreen(isFull) { if (this.fullscreenEnabled) { if (isFull) { this.enterFullscreen(); } else { this.exitFullscreen(); } } } /** * Requests the browser to make this player fullscreen. * * This is not guaranteed to succeed, please check [[fullscreenEnabled]] first. */ enterFullscreen() { const options = { navigationUI: "hide", }; if (this.requestFullscreen) { this.requestFullscreen(options); } else if (this.webkitRequestFullscreen) { this.webkitRequestFullscreen(options); } else if (this.webkitRequestFullScreen) { this.webkitRequestFullScreen(options); } } /** * Requests the browser to no longer make this player fullscreen. */ exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.webkitCancelFullScreen) { document.webkitCancelFullScreen(); } } /** * Called when entering / leaving fullscreen */ fullScreenChange() { var _a; (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_fullscreen(this.isFullscreen); } checkIfTouch(event) { if (event.pointerType === "touch" || event.pointerType === "pen") { this.isTouch = true; } } base64ToBlob(bytesBase64, mimeString) { const byteString = atob(bytesBase64); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } const blob = new Blob([ab], { type: mimeString }); return blob; } /** * Download base-64 string as file * * @param bytesBase64 The base-64 encoded SOL string * @param mimeType The MIME type * @param fileName The name to give the file */ saveFile(bytesBase64, mimeType, fileName) { const blob = this.base64ToBlob(bytesBase64, mimeType); const blobURL = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = blobURL; link.style.display = "none"; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(blobURL); } /** * @returns If the string represent a base-64 encoded SOL file * Check if string is a base-64 encoded SOL file * @param solData The base-64 encoded SOL string */ isB64SOL(solData) { try { const decodedData = atob(solData); return decodedData.slice(6, 10) === "TCSO"; } catch (e) { return false; } } confirmReloadSave(solKey, b64SolData, replace) { if (this.isB64SOL(b64SolData)) { if (localStorage[solKey]) { if (!replace) { const confirmDelete = confirm("Are you sure you want to delete this save file?"); if (!confirmDelete) { return; } } const swfPath = this.swfUrl ? this.swfUrl.pathname : ""; const swfHost = this.swfUrl ? this.swfUrl.hostname : document.location.hostname; const savePath = solKey.split("/").slice(1, -1).join("/"); if (swfPath.includes(savePath) && solKey.startsWith(swfHost)) { const confirmReload = confirm(`The only way to ${replace ? "replace" : "delete"} this save file without potential conflict is to reload this content. Do you wish to continue anyway?`); if (confirmReload && this.loadedConfig) { this.destroy(); replace ? localStorage.setItem(solKey, b64SolData) : localStorage.removeItem(solKey); this.load(this.loadedConfig); this.populateSaves(); this.saveManager.classList.add("hidden"); } return; } replace ? localStorage.setItem(solKey, b64SolData) : localStorage.removeItem(solKey); this.populateSaves(); this.saveManager.classList.add("hidden"); } } } /** * Replace save from SOL file. * * @param event The change event fired * @param solKey The localStorage save file key */ replaceSOL(event, solKey) { const fileInput = event.target; const reader = new FileReader(); reader.addEventListener("load", () => { if (reader.result && typeof reader.result === "string") { const b64Regex = new RegExp("data:.*;base64,"); const b64SolData = reader.result.replace(b64Regex, ""); this.confirmReloadSave(solKey, b64SolData, true); } }); if (fileInput && fileInput.files && fileInput.files.length > 0 && fileInput.files[0]) { reader.readAsDataURL(fileInput.files[0]); } } /** * Delete local save. * * @param key The key to remove from local storage */ deleteSave(key) { const b64SolData = localStorage.getItem(key); if (b64SolData) { this.confirmReloadSave(key, b64SolData, false); } } /** * Puts the local save SOL file keys in a table. */ populateSaves() { const saveTable = this.saveManager.querySelector("#local-saves"); if (!saveTable) { return; } try { localStorage; } catch (e) { return; } saveTable.textContent = ""; Object.keys(localStorage).forEach((key) => { const solName = key.split("/").pop(); const solData = localStorage.getItem(key); if (solName && solData && this.isB64SOL(solData)) { const row = document.createElement("TR"); const keyCol = document.createElement("TD"); keyCol.textContent = solName; keyCol.title = key; const downloadCol = document.createElement("TD"); const downloadSpan = document.createElement("SPAN"); downloadSpan.textContent = "Download"; downloadSpan.className = "save-option"; downloadSpan.addEventListener("click", () => this.saveFile(solData, "application/octet-stream", solName + ".sol")); downloadCol.appendChild(downloadSpan); const replaceCol = document.createElement("TD"); const replaceInput = (document.createElement("INPUT")); replaceInput.type = "file"; replaceInput.accept = ".sol"; replaceInput.className = "replace-save"; replaceInput.id = "replace-save-" + key; const replaceLabel = (document.createElement("LABEL")); replaceLabel.htmlFor = "replace-save-" + key; replaceLabel.textContent = "Replace"; replaceLabel.className = "save-option"; replaceInput.addEventListener("change", (event) => this.replaceSOL(event, key)); replaceCol.appendChild(replaceInput); replaceCol.appendChild(replaceLabel); const deleteCol = document.createElement("TD"); const deleteSpan = document.createElement("SPAN"); deleteSpan.textContent = "Delete"; deleteSpan.className = "save-option"; deleteSpan.addEventListener("click", () => this.deleteSave(key)); deleteCol.appendChild(deleteSpan); row.appendChild(keyCol); row.appendChild(downloadCol); row.appendChild(replaceCol); row.appendChild(deleteCol); saveTable.appendChild(row); } }); } /** * Gets the local save information as SOL files and downloads them. */ backupSaves() { Object.keys(localStorage).forEach((key) => { const solName = key.split("/").pop(); const solData = localStorage.getItem(key); if (solData && this.isB64SOL(solData)) { this.saveFile(solData, "application/octet-stream", solName + ".sol"); } }); } /** * Opens the save manager. */ openSaveManager() { this.saveManager.classList.remove("hidden"); } /** * Fetches the loaded SWF and downloads it. */ async downloadSwf() { try { if (this.swfUrl) { console.log("Downloading SWF: " + this.swfUrl); const response = await fetch(this.swfUrl.href); if (!response.ok) { console.error("SWF download failed"); return; } const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const swfDownloadA = document.createElement("a"); swfDownloadA.style.display = "none"; swfDownloadA.href = blobUrl; swfDownloadA.download = swfFileName(this.swfUrl); document.body.appendChild(swfDownloadA); swfDownloadA.click(); document.body.removeChild(swfDownloadA); URL.revokeObjectURL(blobUrl); } else { console.error("SWF download failed"); } } catch (err) { console.error("SWF download failed"); } } virtualKeyboardInput() { const input = this.virtualKeyboard; const string = input.value; for (const char of string) { for (const eventType of ["keydown", "keyup"]) { this.dispatchEvent(new KeyboardEvent(eventType, { key: char, bubbles: true, })); } } input.value = ""; } openVirtualKeyboard() { // On Android, the Rust code that opens the virtual keyboard triggers // before the TypeScript code that closes it, so delay opening it if (navigator.userAgent.toLowerCase().includes("android")) { setTimeout(() => { this.virtualKeyboard.focus({ preventScroll: true }); }, 100); } else { this.virtualKeyboard.focus({ preventScroll: true }); } } contextMenuItems() { const CHECKMARK = String.fromCharCode(0x2713); const items = []; const addSeparator = () => { // Don't start with or duplicate separators. if (items.length > 0 && items.at(-1) !== null) { items.push(null); } }; if (this.instance) { const customItems = this.instance.prepare_context_menu(); customItems.forEach((item, index) => { if (item.separatorBefore) { addSeparator(); } items.push({ // TODO: better checkboxes text: item.caption + (item.checked ? ` (${CHECKMARK})` : ``), onClick: () => { var _a; return (_a = this.instance) === null || _a === void 0 ? void 0 : _a.run_context_menu_callback(index); }, enabled: item.enabled, }); }); addSeparator(); } if (this.fullscreenEnabled) { if (this.isFullscreen) { items.push({ text: "Exit fullscreen", onClick: () => { var _a; return (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_fullscreen(false); }, }); } else { items.push({ text: "Enter fullscreen", onClick: () => { var _a; return (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_fullscreen(true); }, }); } } if (this.instance && this.swfUrl && this.loadedConfig && this.loadedConfig.showSwfDownload === true) { addSeparator(); items.push({ text: "Download .swf", onClick: this.downloadSwf.bind(this), }); } if (window.isSecureContext) { items.push({ text: "Copy debug info", onClick: () => navigator.clipboard.writeText(this.getPanicData()), }); } this.populateSaves(); const localSaveTable = this.saveManager.querySelector("#local-saves"); if (localSaveTable && localSaveTable.textContent !== "") { items.push({ text: "Open Save Manager", onClick: this.openSaveManager.bind(this), }); } addSeparator(); const extensionString = this.isExtension ? "extension" : ""; items.push({ text: `About Ruffle ${extensionString} (${buildInfo.versionName})`, onClick() { window.open(RUFFLE_ORIGIN, "_blank"); }, }); // Give option to disable context menu when touch support is being used // to avoid a long press triggering the context menu. (#1972) if (this.isTouch) { addSeparator(); items.push({ text: "Hide this menu", onClick: () => (this.contextMenuForceDisabled = true), }); } return items; } pointerDown(event) { this.pointerDownPosition = new Point(event.pageX, event.pageY); this.pointerMoveMaxDistance = 0; this.startLongPressTimer(); } clearLongPressTimer() { if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } } startLongPressTimer() { const longPressTimeout = 800; this.clearLongPressTimer(); this.longPressTimer = setTimeout(() => this.clearLongPressTimer(), longPressTimeout); } checkLongPressMovement(event) { if (this.pointerDownPosition !== null) { const currentPosition = new Point(event.pageX, event.pageY); const distance = this.pointerDownPosition.distanceTo(currentPosition); if (distance > this.pointerMoveMaxDistance) { this.pointerMoveMaxDistance = distance; } } } checkLongPress(event) { const maxAllowedDistance = 15; if (this.longPressTimer) { this.clearLongPressTimer(); // The pointerType condition is to ensure right-click does not trigger // a context menu the wrong way the first time you right-click, // before contextMenuSupported is set. } else if (!this.contextMenuSupported && event.pointerType !== "mouse" && this.pointerMoveMaxDistance < maxAllowedDistance) { this.showContextMenu(event); } } showContextMenu(event) { event.preventDefault(); if (event.type === "contextmenu") { this.contextMenuSupported = true; window.addEventListener("click", this.hideContextMenu.bind(this), { once: true, }); } else { window.addEventListener("pointerup", this.hideContextMenu.bind(this), { once: true }); event.stopPropagation(); } if ((this.loadedConfig && this.loadedConfig.contextMenu === false) || this.contextMenuForceDisabled) { return; } // Clear all context menu items. while (this.contextMenuElement.firstChild) { this.contextMenuElement.removeChild(this.contextMenuElement.firstChild); } // Populate context menu items. for (const item of this.contextMenuItems()) { if (item === null) { const menuSeparator = document.createElement("li"); menuSeparator.className = "menu_separator"; const hr = document.createElement("hr"); menuSeparator.appendChild(hr); this.contextMenuElement.appendChild(menuSeparator); } else { const { text, onClick, enabled } = item; const menuItem = document.createElement("li"); menuItem.className = "menu_item"; menuItem.textContent = text; this.contextMenuElement.appendChild(menuItem); if (enabled !== false) { menuItem.addEventListener(this.contextMenuSupported ? "click" : "pointerup", onClick); } else { menuItem.classList.add("disabled"); } } } // Place a context menu in the top-left corner, so // its `clientWidth` and `clientHeight` are not clamped. this.contextMenuElement.style.left = "0"; this.contextMenuElement.style.top = "0"; this.contextMenuElement.style.display = "block"; const rect = this.getBoundingClientRect(); const x = event.clientX - rect.x; const y = event.clientY - rect.y; const maxX = rect.width - this.contextMenuElement.clientWidth - 1; const maxY = rect.height - this.contextMenuElement.clientHeight - 1; this.contextMenuElement.style.left = Math.floor(Math.min(x, maxX)) + "px"; this.contextMenuElement.style.top = Math.floor(Math.min(y, maxY)) + "px"; } hideContextMenu() { var _a; (_a = this.instance) === null || _a === void 0 ? void 0 : _a.clear_custom_menu_items(); this.contextMenuElement.style.display = "none"; } /** * Pauses this player. * * No more frames, scripts or sounds will be executed. * This movie will be considered inactive and will not wake up until resumed. */ pause() { if (this.instance) { this.instance.pause(); this.playButton.style.display = "block"; } } audioState() { if (this.instance) { const audioContext = this.instance.audio_context(); return (audioContext && audioContext.state) || "running"; } return "suspended"; } unmuteOverlayClicked() { if (this.instance) { if (this.audioState() !== "running") { const audioContext = this.instance.audio_context(); if (audioContext) { audioContext.resume(); } } this.unmuteOverlay.style.display = "none"; } } /** * Plays a silent sound based on the AudioContext's sample rate. * * This is used to unmute audio on iOS and iPadOS when silent mode is enabled on the device (issue 1552). */ unmuteAudioContext() { // No need to play the dummy sound again once audio is unmuted. if (isAudioContextUnmuted) { return; } // TODO: Use `navigator.userAgentData` to detect the platform when support improves? if (navigator.maxTouchPoints < 1) { isAudioContextUnmuted = true; return; } this.container.addEventListener("click", () => { var _a; if (isAudioContextUnmuted) { return; } const audioContext = (_a = this.instance) === null || _a === void 0 ? void 0 : _a.audio_context(); if (!audioContext) { return; } const audio = new Audio(); audio.src = (() => { // Returns a seven samples long 8 bit mono WAVE file. // This is required to prevent the AudioContext from desyncing and crashing. const arrayBuffer = new ArrayBuffer(10); const dataView = new DataView(arrayBuffer); const sampleRate = audioContext.sampleRate; dataView.setUint32(0, sampleRate, true); dataView.setUint32(4, sampleRate, true); dataView.setUint16(8, 1, true); const missingCharacters = window .btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))) .slice(0, 13); return `data:audio/wav;base64,UklGRisAAABXQVZFZm10IBAAAAABAAEA${missingCharacters}AgAZGF0YQcAAACAgICAgICAAAA=`; })(); audio.load(); audio .play() .then(() => { isAudioContextUnmuted = true; }) .catch((err) => { console.warn(`Failed to play dummy sound: ${err}`); }); }, { once: true }); } /** * Copies attributes and children from another element to this player element. * Used by the polyfill elements, RuffleObject and RuffleEmbed. * * @param element The element to copy all attributes from. */ copyElement(element) { if (element) { for (const attribute of element.attributes) { if (attribute.specified) { // Issue 468: Chrome "Click to Active Flash" box stomps on title attribute if (attribute.name === "title" && attribute.value === "Adobe Flash Player") { continue; } try { this.setAttribute(attribute.name, attribute.value); } catch (err) { // The embed may have invalid attributes, so handle these gracefully. console.warn(`Unable to set attribute ${attribute.name} on Ruffle instance`); } } } for (const node of Array.from(element.children)) { this.appendChild(node); } } } /** * Converts a dimension attribute on an HTML embed/object element to a valid CSS dimension. * HTML element dimensions are unitless, but can also be percentages. * Add a 'px' unit unless the value is a percentage. * Returns null if this is not a valid dimension. * * @param attribute The attribute to convert * * @private */ static htmlDimensionToCssDimension(attribute) { if (attribute) { const match = attribute.match(DIMENSION_REGEX); if (match) { let out = match[1]; if (!match[3]) { // Unitless -- add px for CSS. out += "px"; } return out; } } return null; } /** * When a movie presents a new callback through `ExternalInterface.addCallback`, * we are informed so that we can expose the method on any relevant DOM element. * * This should only be called by Ruffle itself and not by users. * * @param name The name of the callback that is now available. * * @internal * @ignore */ onCallbackAvailable(name) { const instance = this.instance; // eslint-disable-next-line @typescript-eslint/no-explicit-any this[name] = (...args) => { return instance === null || instance === void 0 ? void 0 : instance.call_exposed_callback(name, args); }; } /** * Sets a trace observer on this flash player. * * The observer will be called, as a function, for each message that the playing movie will "trace" (output). * * @param observer The observer that will be called for each trace. */ set traceObserver(observer) { var _a; (_a = this.instance) === null || _a === void 0 ? void 0 : _a.set_trace_observer(observer); } /** * Get data included in any panic of this ruffle-player * * @returns A string containing all the data included in the panic. */ getPanicData() { const dataArray = []; dataArray.push("\n# Player Info\n"); dataArray.push(this.debugPlayerInfo()); dataArray.push("\n# Page Info\n"); dataArray.push(`Page URL: ${document.location.href}\n`); if (this.swfUrl) { dataArray.push(`SWF URL: ${this.swfUrl}\n`); } dataArray.push("\n# Browser Info\n"); dataArray.push(`User Agent: ${window.navigator.userAgent}\n`); dataArray.push(`Platform: ${window.navigator.platform}\n`); dataArray.push(`Has touch support: ${window.navigator.maxTouchPoints > 0}\n`); dataArray.push("\n# Ruffle Info\n"); dataArray.push(`Version: ${buildInfo.versionNumber}\n`); dataArray.push(`Name: ${buildInfo.versionName}\n`); dataArray.push(`Channel: ${buildInfo.versionChannel}\n`); dataArray.push(`Built: ${buildInfo.buildDate}\n`); dataArray.push(`Commit: ${buildInfo.commitHash}\n`); dataArray.push(`Is extension: ${this.isExtension}\n`); dataArray.push("\n# Metadata\n"); if (this.metadata) { for (const [key, value] of Object.entries(this.metadata)) { dataArray.push(`${key}: ${value}\n`); } } return dataArray.join(""); } /** * Panics this specific player, forcefully destroying all resources and displays an error message to the user. * * This should be called when something went absolutely, incredibly and disastrously wrong and there is no chance * of recovery. * * Ruffle will attempt to isolate all damage to this specific player instance, but no guarantees can be made if there * was a core issue which triggered the panic. If Ruffle is unable to isolate the cause to a specific player, then * all players will panic and Ruffle will become "poisoned" - no more players will run on this page until it is * reloaded fresh. * * @param error The error, if any, that triggered this panic. */ panic(error) { var _a; if (this.panicked) { // Only show the first major error, not any repeats - they aren't as important return; } this.panicked = true; this.hideSplashScreen(); if (error instanceof Error && (error.name === "AbortError" || error.message.includes("AbortError"))) { // Firefox: Don't display the panic screen if the user leaves the page while something is still loading return; } const errorIndex = (_a = error === null || error === void 0 ? void 0 : error.ruffleIndexError) !== null && _a !== void 0 ? _a : 0 /* PanicError.Unknown */; const errorArray = Object.assign([], { stackIndex: -1, avmStackIndex: -1, }); errorArray.push("# Error Info\n"); if (error instanceof Error) { errorArray.push(`Error name: ${error.name}\n`); errorArray.push(`Error message: ${error.message}\n`); if (error.stack) { const stackIndex = errorArray.push(`Error stack:\n\`\`\`\n${error.stack}\n\`\`\`\n`) - 1; if (error.avmStack) { const avmStackIndex = errorArray.push(`AVM2 stack:\n\`\`\`\n ${error.avmStack .trim() .replace(/\t/g, " ")}\n\`\`\`\n`) - 1; errorArray.avmStackIndex = avmStackIndex; } errorArray.stackIndex = stackIndex; } } else { errorArray.push(`Error: ${error}\n`); } errorArray.push(this.getPanicData()); const errorText = errorArray.join(""); const buildDate = new Date(buildInfo.buildDate); const monthsPrior = new Date(); monthsPrior.setMonth(monthsPrior.getMonth() - 6); // 6 months prior const isBuildOutdated = monthsPrior > buildDate; // Create a link to GitHub with all of the error data, if the build is not outdated. // Otherwise, create a link to the downloads section on the Ruffle website. let actionTag; if (!isBuildOutdated) { // Remove query params for the issue title. const pageUrl = document.location.href.split(/[?#]/)[0]; const issueTitle = `Error on ${pageUrl}`; let issueLink = `https://github.com/ruffle-rs/ruffle/issues/new?title=${encodeURIComponent(issueTitle)}&template=error_report.md&labels=error-report&body=`; let issueBody = encodeURIComponent(errorText); if (errorArray.stackIndex > -1 && String(issueLink + issueBody).length > 8195) { // Strip the stack error from the array when the produced URL is way too long. // This should prevent "414 Request-URI Too Large" errors on GitHub. errorArray[errorArray.stackIndex] = null; if (errorArray.avmStackIndex > -1) { errorArray[errorArray.avmStackIndex] = null; } issueBody = encodeURIComponent(errorArray.join("")); } issueLink += issueBody; actionTag = `Report Bug`; } else { actionTag = `Update Ruffle`; } // Clears out any existing content (ie play button or canvas) and replaces it with the error screen let errorBody, errorFooter; switch (errorIndex) { case 2 /* PanicError.FileProtocol */: // General error: Running on the `file:` protocol errorBody = `

It appears you are running Ruffle on the "file:" protocol.

This doesn't work as browsers block many features from working for security reasons.

Instead, we invite you to setup a local server or either use the web demo or the desktop application.

`; errorFooter = `
  • Web Demo
  • Desktop Application
  • `; break; case 4 /* PanicError.JavascriptConfiguration */: // General error: Incorrect JavaScript configuration errorBody = `

    Ruffle has encountered a major issue due to an incorrect JavaScript configuration.

    If you are the server administrator, we invite you to check the error details to find out which parameter is at fault.

    You can also consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; case 9 /* PanicError.WasmNotFound */: // Self hosted: Cannot load `.wasm` file - file not found errorBody = `

    Ruffle failed to load the required ".wasm" file component.

    If you are the server administrator, please ensure the file has correctly been uploaded.

    If the issue persists, you may need to use the "publicPath" setting: please consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; case 8 /* PanicError.WasmMimeType */: // Self hosted: Cannot load `.wasm` file - incorrect MIME type errorBody = `

    Ruffle has encountered a major issue whilst trying to initialize.

    This web server is not serving ".wasm" files with the correct MIME type.

    If you are the server administrator, please consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; case 11 /* PanicError.SwfFetchError */: errorBody = `

    Ruffle failed to load the Flash SWF file.

    The most likely reason is that the file no longer exists, so there is nothing for Ruffle to load.

    Try contacting the website administrator for help.

    `; errorFooter = `
  • View Error Details
  • `; break; case 12 /* PanicError.SwfCors */: // Self hosted: Cannot load SWF file - CORS issues errorBody = `

    Ruffle failed to load the Flash SWF file.

    Access to fetch has likely been blocked by CORS policy.

    If you are the server administrator, please consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; case 6 /* PanicError.WasmCors */: // Self hosted: Cannot load `.wasm` file - CORS issues errorBody = `

    Ruffle failed to load the required ".wasm" file component.

    Access to fetch has likely been blocked by CORS policy.

    If you are the server administrator, please consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; case 3 /* PanicError.InvalidWasm */: // Self hosted: Cannot load `.wasm` file - incorrect configuration or missing files errorBody = `

    Ruffle has encountered a major issue whilst trying to initialize.

    It seems like this page has missing or invalid files for running Ruffle.

    If you are the server administrator, please consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; case 7 /* PanicError.WasmDownload */: // Usually a transient network error or botched deployment errorBody = `

    Ruffle has encountered a major issue whilst trying to initialize.

    This can often resolve itself, so you can try reloading the page.

    Otherwise, please contact the website administrator.

    `; errorFooter = `
  • View Error Details
  • `; break; case 10 /* PanicError.WasmDisabledMicrosoftEdge */: // Self hosted: User has disabled WebAssembly in Microsoft Edge through the // "Enhance your Security on the web" setting. errorBody = `

    Ruffle failed to load the required ".wasm" file component.

    To fix this, try opening your browser's settings, clicking "Privacy, search, and services", scrolling down, and turning off "Enhance your security on the web".

    This will allow your browser to load the required ".wasm" files.

    If the issue persists, you might have to use a different browser.

    `; errorFooter = `
  • More Information
  • View Error Details
  • `; break; case 5 /* PanicError.JavascriptConflict */: // Self hosted: Cannot load `.wasm` file - a native object / function is overriden errorBody = `

    Ruffle has encountered a major issue whilst trying to initialize.

    It seems like this page uses JavaScript code that conflicts with Ruffle.

    If you are the server administrator, we invite you to try loading the file on a blank page.

    `; if (isBuildOutdated) { errorBody += `

    You can also try to upload a more recent version of Ruffle that may circumvent the issue (current build is outdated: ${buildInfo.buildDate}).

    `; } errorFooter = `
  • ${actionTag}
  • View Error Details
  • `; break; case 1 /* PanicError.CSPConflict */: // General error: Cannot load `.wasm` file - a native object / function is overriden errorBody = `

    Ruffle has encountered a major issue whilst trying to initialize.

    This web server's Content Security Policy does not allow the required ".wasm" component to run.

    If you are the server administrator, please consult the Ruffle wiki for help.

    `; errorFooter = `
  • View Ruffle Wiki
  • View Error Details
  • `; break; default: // Unknown error errorBody = `

    Ruffle has encountered a major issue whilst trying to display this Flash content.

    `; if (!isBuildOutdated) { errorBody += `

    This isn't supposed to happen, so we'd really appreciate if you could file a bug!

    `; } else { errorBody += `

    If you are the server administrator, please try to upload a more recent version of Ruffle (current build is outdated: ${buildInfo.buildDate}).

    `; } errorFooter = `
  • ${actionTag}
  • View Error Details
  • `; break; } this.container.innerHTML = `
    Something went wrong :(
    ${errorBody}
    `; const viewDetails = (this.container.querySelector("#panic-view-details")); if (viewDetails) { viewDetails.onclick = () => { const panicBody = (this.container.querySelector("#panic-body")); panicBody.classList.add("details"); const panicText = document.createElement("textarea"); panicText.value = errorText; panicBody.appendChild(panicText); return false; }; } // Do this last, just in case it causes any cascading issues. this.destroy(); } displayRootMovieDownloadFailedMessage() { if (this.isExtension && window.location.origin !== this.swfUrl.origin) { this.hideSplashScreen(); const div = document.createElement("div"); div.id = "message_overlay"; div.innerHTML = `

    Ruffle wasn't able to run the Flash embedded in this page.

    You can try to open the file in a separate tab, to sidestep this issue.

    Open in a new tab
    `; this.container.prepend(div); } else { const error = new Error("Failed to fetch: " + this.swfUrl); if (!this.swfUrl.protocol.includes("http")) { error.ruffleIndexError = 2 /* PanicError.FileProtocol */; } else if (window.location.origin === this.swfUrl.origin) { error.ruffleIndexError = 11 /* PanicError.SwfFetchError */; } else { // This is a selfhosted build of Ruffle that tried to make a cross-origin request error.ruffleIndexError = 12 /* PanicError.SwfCors */; } this.panic(error); } } displayUnsupportedMessage() { const div = document.createElement("div"); div.id = "message_overlay"; // TODO: Change link to https://ruffle.rs/faq or similar // TODO: Pause content until message is dismissed div.innerHTML = `

    The Ruffle emulator may not yet fully support all of ActionScript 3 used by this content.

    Some parts of the content may not work as expected.

    More info
    `; this.container.prepend(div); const button = div.querySelector("#run-anyway-btn"); button.onclick = () => { div.parentNode.removeChild(div); }; } /** * Show a dismissible message in front of the player. * * @param message The message shown to the user. */ displayMessage(message) { const div = document.createElement("div"); div.id = "message_overlay"; div.innerHTML = `

    ${message}

    `; this.container.prepend(div); (this.container.querySelector("#continue-btn")).onclick = () => { div.parentNode.removeChild(div); }; } debugPlayerInfo() { let result = `Allows script access: ${this.loadedConfig ? this.loadedConfig.allowScriptAccess : false}\n`; let renderInfo = `(Cached) ${this._cachedDebugInfo}`; if (this.instance) { try { renderInfo = this.instance.renderer_debug_info(); } catch (_a) { // ignored } } if (renderInfo) { result += `${renderInfo}\n`; } return result; } hideSplashScreen() { this.splashScreen.classList.add("hidden"); this.container.classList.remove("hidden"); } showSplashScreen() { this.splashScreen.classList.remove("hidden"); this.container.classList.add("hidden"); } setMetadata(metadata) { this._metadata = metadata; // TODO: Switch this to ReadyState.Loading when we have streaming support. this._readyState = 2 /* ReadyState.Loaded */; this.hideSplashScreen(); this.dispatchEvent(new Event(RufflePlayer.LOADED_METADATA)); // TODO: Move this to whatever function changes the ReadyState to Loaded when we have streaming support. this.dispatchEvent(new Event(RufflePlayer.LOADED_DATA)); } setIsExtension(isExtension) { this.isExtension = isExtension; } } /** * Triggered when a movie metadata has been loaded (such as movie width and height). * * @event RufflePlayer#loadedmetadata */ RufflePlayer.LOADED_METADATA = "loadedmetadata"; /** * Triggered when a movie is fully loaded. * * @event RufflePlayer#loadeddata */ RufflePlayer.LOADED_DATA = "loadeddata"; /** * Returns whether a SWF file can call JavaScript code in the surrounding HTML file. * * @param access The value of the `allowScriptAccess` attribute. * @param url The URL of the SWF file. * @returns True if script access is allowed. */ function isScriptAccessAllowed(access, url) { if (!access) { access = "sameDomain"; } switch (access.toLowerCase()) { case "always": return true; case "never": return false; case "samedomain": default: try { return (new URL(window.location.href).origin === new URL(url, window.location.href).origin); } catch (_a) { return false; } } } /** * Returns whether a SWF file should show the built-in context menu items. * * @param menu The value of the `menu` attribute. * @returns True if the built-in context items should be shown. */ function isBuiltInContextMenuVisible(menu) { if (menu === null || menu.toLowerCase() === "true") { return true; } return false; } /** * Returns whether the given filename is a Youtube Flash source. * * @param filename The filename to test. * @returns True if the filename is a Youtube Flash source. */ function isYoutubeFlashSource(filename) { if (filename) { let pathname = ""; let cleaned_hostname = ""; try { // A base URL is required if `filename` is a relative URL, but we don't need to detect the real URL origin. const url = new URL(filename, RUFFLE_ORIGIN); pathname = url.pathname; cleaned_hostname = url.hostname.replace("www.", ""); } catch (err) { // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation. } // See https://wiki.mozilla.org/QA/Youtube_Embedded_Rewrite if (pathname.startsWith("/v/") && (cleaned_hostname === "youtube.com" || cleaned_hostname === "youtube-nocookie.com")) { return true; } } return false; } /** * Workaround Youtube mixed content if upgradeToHttps is true. * * @param elem The element to change. * @param attr The attribute to adjust. */ function workaroundYoutubeMixedContent(elem, attr) { var _a, _b; const elem_attr = elem.getAttribute(attr); const window_config = (_b = (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {}; if (elem_attr) { try { const url = new URL(elem_attr); if (url.protocol === "http:" && window.location.protocol === "https:" && (!("upgradeToHttps" in window_config) || window_config.upgradeToHttps !== false)) { url.protocol = "https:"; elem.setAttribute(attr, url.toString()); } } catch (err) { // Some invalid filenames, like `///`, could raise a TypeError. Let's fail silently in this situation. } } } /** * Determine if an element is a child of a node that was not supported * in non-HTML5 compliant browsers. If so, the element was meant to be * used as a fallback content. * * @param elem The element to test. * @returns True if the element is inside an