`.style(n)}});let kn=[],_n={};function On(e){for(let n of e.changedTouches)kn[n.identifier]=n.target}function In(e){for(let n of e.changedTouches)Cn(n.target,n.screenX,n.screenY)}function Mn(e){for(let n of e.changedTouches)kn.splice(n.identifier,1)}function jn(e){kn[20]=e.target}function Tn(e){Cn(kn[20],e.screenX,e.screenY)}function An(){kn.splice(20,1)}function Cn(e,n,t){if(!e)return;let o=0,r=0;e._previousPos&&(o=n-e._previousPos.x,r=t-e._previousPos.y,o=fn(o,-6,6)/1,r=fn(r,-6,6)/1),e._update={dx:o,dy:r,x:n,y:t}}function Dn(e){e.preventDefault()}function Pn(e){if(!e.deviceId)return void alert("No MIDI device id, MIDI output will fail!");let n=!1;for(let t of Qe.outputs.values())t.id===e.deviceId&&(n=!0);if(n){if(function(e="output-1",n=0){en=e,nn=n}(Qe.outputs.get(e.deviceId),e.globalChannel||0),console.log(`MIDI configured for device '${e.deviceId}' and channel: ${e.globalChannel}`),e.restoreValues){console.log("Restoring saved widget values...");for(let e of document.body.querySelectorAll("midi-slider,midi-encoder,midi-pad,midi-counter"))if("midi-pad"==e.tagName.toLowerCase()){let n=hn(e,`${e.ccX}${e.chan}${e.nrpn}X`),t=hn(e,`${e.ccY}${e.chan}${e.nrpn}Y`);n&&(e.valueX=n),t&&(e.valueY=t)}else{let n=hn(e);n&&(e.value=n)}}}else alert(`The provided MIDI device id '${e.deviceId}' is invalid, MIDI output will fail!`)}function Ln(){for(let e of document.body.querySelectorAll("midi-slider,midi-encoder,midi-pad,midi-button,midi-counter"))e._width=e.clientWidth}window.addEventListener("load",(async()=>{await async function(){const e=document.createElement("div");e.id="pageMask",document.body.append(e);const n=document.createElement("style");n.textContent="@import url('https://fonts.googleapis.com/css2?family=PT+Sans&display=swap');\n\n:root {\n /* these variables are used everywhere */\n --bg: black; /* background colour */\n --b-radius: min(1.9vmin, 15px); /* border radius */\n --b-width: 0.3rem; /* border thickness */\n --spacing: 0.3rem; /* widget spacing */\n\n --font: 'PT Sans', sans-serif; /* Consolas, 'Courier New', monospace; */\n}\n\nhtml,\nbody {\n height: 100%;\n margin: 0;\n padding: 0;\n overflow-y: hidden;\n color: white;\n background-color: var(--bg);\n font-family: var(--font);\n touch-action: none;\n}\n\nbody {\n display: flex;\n flex-direction: column;\n}\n\n/* Used to hide the page behind the config dialog */\n#pageMask {\n background: rgba(0, 0, 0, 0.8);\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n display: none;\n}\n";let t=document.createElement("meta");t.setAttribute("name","viewport"),t.setAttribute("content","width=device-width, user-scalable=no, initial-scale=1.0"),document.getElementsByTagName("head")[0].appendChild(t);let o=document.querySelector("link[rel~='icon']");o||(o=document.createElement("link"),o.rel="icon",o.href="https://raw.githubusercontent.com/benc-uk/touchmidi/main/src/assets/favicon.png",document.getElementsByTagName("head")[0].appendChild(o)),document.head.append(n)}();const e=await on();if(e){for(let n of e.outputs.values())console.log(`MIDI device found --- deviceId: ${n.id}, name: ${n.name}`);null===new URLSearchParams(window.location.search).get("nomidi")&&function(e){const n=new URLSearchParams(window.location.search);if(n.has("channel")||n.has("device")||n.has("restore")){_n=yn,_n.globalChannel=n.has("channel")?parseInt(n.get("channel")):1,_n.deviceId=n.has("device")?n.get("device"):_n.deviceId,_n.restoreValues=n.has("restore")?"true"===n.get("restore"):_n.restoreValues,_n.restoreValues||mn();const e=window.location.pathname.substring(window.location.pathname.lastIndexOf("/")+1);return localStorage.setItem(`touchmidi.${e}.config`,JSON.stringify(_n)),void Pn(_n)}const t=document.createElement("midi-config");t.channelNames=void 0,t.addEventListener("config-done",(e=>{Pn(e.detail)})),document.body.appendChild(t)}(),window.addEventListener("resize",Ln),setTimeout(Ln,200),window.addEventListener("mousedown",jn,!1),window.addEventListener("mousemove",Tn,!1),window.addEventListener("mouseup",An,!1),window.addEventListener("touchstart",On,!1),window.addEventListener("touchmove",In,!1),window.addEventListener("touchend",Mn,!1),window.addEventListener("dblclick",Dn,!1),window.addEventListener("contextmenu",Dn,!1)}else document.body.innerHTML='
\n
Failed to get MIDI access 😯
\n This is likely because your browser doesn\'t support MIDI or permissions were not granted.\n Also ensure you load the page from a https:// or file:// URL origin.
Try again using Chrome or Edge
'}))}},n={};function t(o){if(n[o])return n[o].exports;var r=n[o]={exports:{}};return e[o](r,r.exports,t),r.exports}t.d=(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})},t.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t(305)})();
\ No newline at end of file
diff --git a/webmidi/midipanel_files/ruffle.js b/webmidi/midipanel_files/ruffle.js
new file mode 100644
index 0000000..ee89abe
--- /dev/null
+++ b/webmidi/midipanel_files/ruffle.js
@@ -0,0 +1,4282 @@
+/******/ (() => { // 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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
+ Backup all saves (download all sols)
+
+
+
+
+
+`;
+
+;// 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.
+ `;
+ 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.