4282 lines
166 KiB
JavaScript
4282 lines
166 KiB
JavaScript
/******/ (() => { // 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 = `
|
|
<style>
|
|
:host {
|
|
all: initial;
|
|
|
|
--ruffle-blue: #37528c;
|
|
--ruffle-orange: #ffad33;
|
|
|
|
display: inline-block;
|
|
position: relative;
|
|
/* Default width/height; this will get overridden by user styles/attributes. */
|
|
width: 550px;
|
|
height: 400px;
|
|
font-family: Arial, sans-serif;
|
|
letter-spacing: 0.4px;
|
|
touch-action: none;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* Ruffle's width/height CSS interferes with Safari's fullscreen CSS. */
|
|
/* Ensure that Safari's fullscreen mode actually fills the screen. */
|
|
:host(:-webkit-full-screen) {
|
|
display: block;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
}
|
|
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* All of these use the dimensions specified by the embed. */
|
|
#container,
|
|
#play_button,
|
|
#unmute_overlay,
|
|
#unmute_overlay .background,
|
|
#panic,
|
|
#splash-screen,
|
|
#message_overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
|
|
#container {
|
|
overflow: hidden;
|
|
}
|
|
|
|
#container canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
#play_button,
|
|
#unmute_overlay {
|
|
cursor: pointer;
|
|
display: none;
|
|
}
|
|
|
|
#unmute_overlay .background {
|
|
background: black;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
#play_button .icon,
|
|
#unmute_overlay .icon {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 50%;
|
|
height: 50%;
|
|
max-width: 384px;
|
|
max-height: 384px;
|
|
transform: translate(-50%, -50%);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
#play_button:hover .icon,
|
|
#unmute_overlay:hover .icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
#panic {
|
|
font-size: 20px;
|
|
text-align: center;
|
|
/* Inverted colors from play button! */
|
|
background: linear-gradient(180deg, #fd3a40 0%, #fda138 100%);
|
|
color: white;
|
|
display: flex;
|
|
flex-flow: column;
|
|
justify-content: space-around;
|
|
}
|
|
|
|
#panic a {
|
|
color: var(--ruffle-blue);
|
|
font-weight: bold;
|
|
}
|
|
|
|
#panic-title {
|
|
font-size: xxx-large;
|
|
font-weight: bold;
|
|
}
|
|
|
|
#panic-body.details {
|
|
flex: 0.9;
|
|
margin: 0 10px;
|
|
}
|
|
|
|
#panic-body textarea {
|
|
width: 100%;
|
|
height: 100%;
|
|
resize: none;
|
|
}
|
|
|
|
#panic ul {
|
|
padding: 0;
|
|
display: flex;
|
|
list-style-type: none;
|
|
justify-content: space-evenly;
|
|
}
|
|
|
|
#message_overlay {
|
|
position: absolute;
|
|
background: var(--ruffle-blue);
|
|
color: var(--ruffle-orange);
|
|
opacity: 1;
|
|
z-index: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: auto;
|
|
}
|
|
|
|
#message_overlay .message {
|
|
text-align: center;
|
|
max-height: 100%;
|
|
max-width: 100%;
|
|
padding: 5%;
|
|
font-size: 20px;
|
|
}
|
|
|
|
#message_overlay p {
|
|
margin: 0.5em 0;
|
|
}
|
|
|
|
#message_overlay .message div {
|
|
display: flex;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
column-gap: 1em;
|
|
}
|
|
|
|
#message_overlay a, #message_overlay button {
|
|
cursor: pointer;
|
|
background: var(--ruffle-blue);
|
|
color: var(--ruffle-orange);
|
|
border: 2px solid var(--ruffle-orange);
|
|
font-weight: bold;
|
|
font-size: 1.25em;
|
|
border-radius: 0.6em;
|
|
padding: 10px;
|
|
text-decoration: none;
|
|
margin: 2% 0;
|
|
}
|
|
|
|
#message_overlay a:hover, #message_overlay button:hover {
|
|
background: #ffffff4c;
|
|
}
|
|
|
|
#continue-btn {
|
|
cursor: pointer;
|
|
background: var(--ruffle-blue);
|
|
color: var(--ruffle-orange);
|
|
border: 2px solid var(--ruffle-orange);
|
|
font-weight: bold;
|
|
font-size: 20px;
|
|
border-radius: 20px;
|
|
padding: 10px;
|
|
}
|
|
|
|
#continue-btn:hover {
|
|
background: #ffffff4c;
|
|
}
|
|
|
|
#context-menu {
|
|
display: none;
|
|
color: black;
|
|
background: #fafafa;
|
|
border: 1px solid gray;
|
|
box-shadow: 0px 5px 10px -5px black;
|
|
position: absolute;
|
|
font-size: 14px;
|
|
text-align: left;
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
#context-menu .menu_item {
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
color: black;
|
|
}
|
|
|
|
#context-menu .menu_item.disabled {
|
|
cursor: default;
|
|
color: gray;
|
|
}
|
|
|
|
#context-menu .menu_item:not(.disabled):hover {
|
|
background: lightgray;
|
|
}
|
|
|
|
#context-menu .menu_separator hr {
|
|
border: none;
|
|
border-bottom: 1px solid lightgray;
|
|
margin: 2px;
|
|
}
|
|
|
|
#splash-screen {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--splash-screen-background, var(--preloader-background, var(--ruffle-blue)));
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.loadbar {
|
|
width: 100%;
|
|
max-width: 316px;
|
|
max-height: 10px;
|
|
height: 20%;
|
|
background: #253559;
|
|
}
|
|
|
|
.loadbar-inner {
|
|
width: 0px;
|
|
max-width: 100%;
|
|
height: 100%;
|
|
background: var(--ruffle-orange);
|
|
}
|
|
|
|
.logo {
|
|
display: var(--logo-display, block);
|
|
max-width: 380px;
|
|
max-height: 150px;
|
|
}
|
|
|
|
.loading-animation {
|
|
max-width: 28px;
|
|
max-height: 28px;
|
|
margin-bottom: 2%;
|
|
width: 10%;
|
|
aspect-ratio: 1;
|
|
}
|
|
|
|
.spinner {
|
|
stroke-dasharray: 180;
|
|
stroke-dashoffset: 135;
|
|
stroke: var(--ruffle-orange);
|
|
transform-origin: 50% 50%;
|
|
animation: rotate 1.5s linear infinite;
|
|
}
|
|
|
|
@keyframes rotate {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
#virtual-keyboard {
|
|
position: absolute;
|
|
opacity: 0;
|
|
top: -100px;
|
|
width: 1px;
|
|
height: 1px;
|
|
}
|
|
|
|
#save-manager {
|
|
height: inherit;
|
|
user-select: text;
|
|
}
|
|
#modal-area {
|
|
position: sticky;
|
|
background: white;
|
|
width: fit-content;
|
|
padding: 16px;
|
|
border: 3px solid black;
|
|
margin: auto;
|
|
height: 500px;
|
|
max-height: calc(100% - 38px);
|
|
min-height: 80px;
|
|
}
|
|
|
|
#restore-save {
|
|
display: none;
|
|
}
|
|
|
|
.replace-save {
|
|
display: none;
|
|
}
|
|
|
|
.save-option {
|
|
display: inline-block;
|
|
padding: 3px 10px;
|
|
margin: 5px 2px;
|
|
cursor: pointer;
|
|
border-radius: 50px;
|
|
background-color: var(--ruffle-blue);
|
|
color: white;
|
|
}
|
|
|
|
#close-modal {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 10px;
|
|
cursor: pointer;
|
|
font-size: x-large;
|
|
}
|
|
|
|
.general-save-options {
|
|
text-align: center;
|
|
padding-bottom: 8px;
|
|
border-bottom: 2px solid #888;
|
|
}
|
|
|
|
#local-saves {
|
|
border-collapse: collapse;
|
|
overflow-y: auto;
|
|
display: block;
|
|
padding-right: 16px;
|
|
height: calc(100% - 45px);
|
|
min-height: 30px;
|
|
}
|
|
|
|
#local-saves td {
|
|
border-bottom: 1px solid #bbb;
|
|
height: 30px;
|
|
}
|
|
|
|
#local-saves tr td:nth-child(1) {
|
|
padding-right: 1em;
|
|
word-break: break-all;
|
|
}
|
|
|
|
#local-saves tr:nth-child(even) {
|
|
background-color: #f2f2f2;
|
|
}
|
|
</style>
|
|
<style id="dynamic_styles"></style>
|
|
|
|
<div id="container">
|
|
<div id="play_button"><div class="icon"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" viewBox="0 0 250 250" width="100%" height="100%"><defs><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="125" y1="0" x2="125" y2="250" spreadMethod="pad"><stop offset="0%" stop-color="#FDA138"/><stop offset="100%" stop-color="#FD3A40"/></linearGradient><g id="b"><path fill="url(#a)" d="M250 125q0-52-37-88-36-37-88-37T37 37Q0 73 0 125t37 88q36 37 88 37t88-37q37-36 37-88M87 195V55l100 70-100 70z"/><path fill="#FFF" d="M87 55v140l100-70L87 55z"/></g></defs><use xlink:href="#b"/></svg></div></div>
|
|
<div id="unmute_overlay"><div class="background"></div><div class="icon"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" viewBox="0 0 512 584" width="100%" height="100%" scale="0.8"><path fill="#FFF" stroke="#FFF" d="m457.941 256 47.029-47.029c9.372-9.373 9.372-24.568 0-33.941-9.373-9.373-24.568-9.373-33.941 0l-47.029 47.029-47.029-47.029c-9.373-9.373-24.568-9.373-33.941 0-9.372 9.373-9.372 24.568 0 33.941l47.029 47.029-47.029 47.029c-9.372 9.373-9.372 24.568 0 33.941 4.686 4.687 10.827 7.03 16.97 7.03s12.284-2.343 16.971-7.029l47.029-47.03 47.029 47.029c4.687 4.687 10.828 7.03 16.971 7.03s12.284-2.343 16.971-7.029c9.372-9.373 9.372-24.568 0-33.941z"/><path fill="#FFF" stroke="#FFF" d="m99 160h-55c-24.301 0-44 19.699-44 44v104c0 24.301 19.699 44 44 44h55c2.761 0 5-2.239 5-5v-182c0-2.761-2.239-5-5-5z"/><path fill="#FFF" stroke="#FFF" d="m280 56h-24c-5.269 0-10.392 1.734-14.578 4.935l-103.459 79.116c-1.237.946-1.963 2.414-1.963 3.972v223.955c0 1.557.726 3.026 1.963 3.972l103.459 79.115c4.186 3.201 9.309 4.936 14.579 4.936h23.999c13.255 0 24-10.745 24-24v-352.001c0-13.255-10.745-24-24-24z"/><text x="256" y="560" text-anchor="middle" font-size="60px" fill="#FFF" stroke="#FFF">Click to unmute</text></svg></div></div>
|
|
<input id="virtual-keyboard" type="text" autocapitalize="off" autocomplete="off" autocorrect="off">
|
|
</div>
|
|
<div class="hidden" id="splash-screen">
|
|
<svg class="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid" viewBox="0 0 380 150"><g><path fill="#966214" d="M58.75 85.6q.75-.1 1.5-.35.85-.25 1.65-.75.55-.35 1.05-.8.5-.45.95-1 .5-.5.75-1.2-.05.05-.15.1-.1.15-.25.25l-.1.2q-.15.05-.25.1-.4 0-.8.05-.5-.25-.9-.5-.3-.1-.55-.3l-.6-.6-4.25-6.45-1.5 11.25h3.45m83.15-.2h3.45q.75-.1 1.5-.35.25-.05.45-.15.35-.15.65-.3l.5-.3q.25-.15.5-.35.45-.35.9-.75.45-.35.75-.85l.1-.1q.1-.2.2-.35.2-.3.35-.6l-.3.4-.15.15q-.5.15-1.1.1-.25 0-.4-.05-.5-.15-.8-.4-.15-.1-.25-.25-.3-.3-.55-.6l-.05-.05v-.05l-4.25-6.4-1.5 11.25m-21.15-3.95q-.3-.3-.55-.6l-.05-.05v-.05l-4.25-6.4-1.5 11.25h3.45q.75-.1 1.5-.35.85-.25 1.6-.75.75-.5 1.4-1.1.45-.35.75-.85.35-.5.65-1.05l-.45.55q-.5.15-1.1.1-.9 0-1.45-.7m59.15.3q-.75-.5-1.4-1-3.15-2.55-3.5-6.4l-1.5 11.25h21q-3.1-.25-5.7-.75-5.6-1.05-8.9-3.1m94.2 3.85h3.45q.6-.1 1.2-.3.4-.1.75-.2.35-.15.65-.3.7-.35 1.35-.8.75-.55 1.3-1.25.1-.15.25-.3-2.55-.25-3.25-1.8l-4.2-6.3-1.5 11.25m-45.3-4.85q-.5-.4-.9-.8-2.3-2.35-2.6-5.6l-1.5 11.25h21q-11.25-.95-16-4.85m97.7 4.85q-.3-.05-.6-.05-10.8-1-15.4-4.8-3.15-2.55-3.5-6.35l-1.5 11.2h21Z"/><path fill="var(--ruffle-orange)" d="M92.6 54.8q-1.95-1.4-4.5-1.4H60.35q-1.35 0-2.6.45-1.65.55-3.15 1.8-2.75 2.25-3.25 5.25l-1.65 12h.05v.3l5.85 1.15h-9.5q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45-.5.3-.95.7-.45.35-.85.8-.35.4-.65.85-.3.45-.5.9-.15.45-.3.95l-5.85 41.6H50.3l5-35.5 1.5-11.25 4.25 6.45.6.6q.25.2.55.3.4.25.9.5.4-.05.8-.05.1-.05.25-.1l.1-.2q.15-.1.25-.25.1-.05.15-.1l.3-1.05 1.75-12.3h11.15L75.8 82.6h16.5l2.3-16.25h-.05l.8-5.7q.4-2.45-1-4.2-.35-.4-.75-.8-.25-.25-.55-.5-.2-.2-.45-.35m16.2 18.1h.05l-.05.3 5.85 1.15H105.2q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45-.5.3-1 .65-.4.4-.8.85-.25.3-.55.65-.05.1-.15.2-.25.45-.4.9-.2.45-.3.95-.1.65-.2 1.25-.2 1.15-.4 2.25l-4.3 30.6q-.25 3 1.75 5.25 1.6 1.8 4 2.15.6.1 1.25.1h27.35q3.25 0 6-2.25.35-.35.7-.55l.3-.2q2-2 2.25-4.5l1.65-11.6q.05-.05.1-.05l1.65-11.35h.05l.7-5.2 1.5-11.25 4.25 6.4v.05l.05.05q.25.3.55.6.1.15.25.25.3.25.8.4.15.05.4.05.6.05 1.1-.1l.15-.15.3-.4.3-1.05 1.3-9.05h-.05l.7-5.05h-.05l.15-1.25h-.05l1.65-11.7h-16.25l-2.65 19.5h.05v.2l-.05.1h.05l5.8 1.15H132.7q-.5.05-1 .15-.5.15-1 .35-.15.05-.3.15-.3.1-.55.25-.05 0-.1.05-.5.3-1 .65-.4.35-.7.7-.55.7-.95 1.45-.35.65-.55 1.4-.15.7-.25 1.4v.05q-.15 1.05-.35 2.05l-1.2 8.75v.1l-2.1 14.7H111.4l2.25-15.55h.05l.7-5.2 1.5-11.25 4.25 6.4v.05l.05.05q.25.3.55.6.55.7 1.45.7.6.05 1.1-.1l.45-.55.3-1.05 1.3-9.05h-.05l.7-5.05h-.05l.15-1.25h-.05l1.65-11.7h-16.25l-2.65 19.5m106.5-41.75q-2.25-2.25-5.5-2.25h-27.75q-3 0-5.75 2.25-1.3.95-2.05 2.1-.45.6-.7 1.2-.2.5-.35 1-.1.45-.15.95l-4.15 29.95h-.05l-.7 5.2h-.05l-.2 1.35h.05l-.05.3 5.85 1.15h-9.45q-2.1.05-3.95 1.6-1.9 1.55-2.25 3.55l-.5 3.5h-.05l-5.3 38.1h16.25l5-35.5 1.5-11.25q.35 3.85 3.5 6.4.65.5 1.4 1 3.3 2.05 8.9 3.1 2.6.5 5.7.75l1.75-11.25h-12.2l.4-2.95h-.05l.7-5.05h-.05q.1-.9.3-1.9.1-.75.2-1.6.85-5.9 2.15-14.9 0-.15.05-.25l.1-.9q.2-1.55.45-3.15h11.25l-3.1 20.8h16.5l4.1-28.05q.15-1.7-.4-3.15-.5-1.1-1.35-2.1m46.65 44.15q-.5.3-1 .65-.4.4-.8.85-.35.4-.7.85-.25.45-.45.9-.15.45-.3.95l-5.85 41.6h16.25l5-35.5 1.5-11.25 4.2 6.3q.7 1.55 3.25 1.8l.05-.1q.25-.4.35-.85l.3-1.05 1.8-14.05v-.05l5.35-37.45h-16.25l-6.15 44.3 5.85 1.15h-9.45q-.5.05-1 .15-.5.15-1 .35-.5.2-.95.45m5.4-38.9q.15-1.7-.4-3.15-.5-1.1-1.35-2.1-2.25-2.25-5.5-2.25h-27.75q-2.3 0-4.45 1.35-.65.35-1.3.9-1.3.95-2.05 2.1-.45.6-.7 1.2-.4.9-.5 1.95l-4.15 29.95h-.05l-.7 5.2h-.05l-.2 1.35h.05l-.05.3 5.85 1.15h-9.45q-2.1.05-3.95 1.6-1.9 1.55-2.25 3.55l-.5 3.5h-.05l-1.2 8.75v.1l-4.1 29.25h16.25l5-35.5 1.5-11.25q.3 3.25 2.6 5.6.4.4.9.8 4.75 3.9 16 4.85l1.75-11.25h-12.2l.4-2.95h-.05l.7-5.05h-.05q.15-.9.3-1.9.1-.75.25-1.6.15-1.25.35-2.65v-.05q.95-6.7 2.35-16.5h11.25l-3.1 20.8h16.5l4.1-28.05M345 66.35h-.05l1.15-8.2q.5-3-1.75-5.25-1.25-1.25-3-1.75-1-.5-2.25-.5h-27.95q-.65 0-1.3.1-2.5.35-4.7 2.15-2.75 2.25-3.25 5.25l-1.95 14.7v.05l-.05.3 5.85 1.15h-9.45q-1.9.05-3.6 1.35-.2.1-.35.25-1.9 1.55-2.25 3.55l-4.85 34.1q-.25 3 1.75 5.25 1.25 1.4 3 1.95 1.05.3 2.25.3H320q3.25 0 6-2.25 2.75-2 3.25-5l2.75-18.5h-16.5l-1.75 11H302.5l2.1-14.75h.05l.85-6 1.5-11.2q.35 3.8 3.5 6.35 4.6 3.8 15.4 4.8.3 0 .6.05h15.75L345 66.35m-16.4-.95-1.25 8.95h-11.3l.4-2.95h-.05l.7-5.05h-.1l.15-.95h11.45Z"/></g></svg>
|
|
<svg class="loading-animation" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66 66">
|
|
<circle class="spinner" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
|
|
</svg>
|
|
<div class="loadbar"><div class="loadbar-inner"></div></div>
|
|
</div>
|
|
|
|
<div id="save-manager" class="hidden">
|
|
<div id="modal-area">
|
|
<span id="close-modal">×</span>
|
|
<div class="general-save-options">
|
|
<span class="save-option" id="backup-saves">Backup all saves (download all sols)</span>
|
|
</div>
|
|
<table id="local-saves"></table>
|
|
</div>
|
|
</div>
|
|
<ul id="context-menu"></ul>
|
|
`;
|
|
|
|
;// 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 = `<a target="_top" href="${issueLink}">Report Bug</a>`;
|
|
}
|
|
else {
|
|
actionTag = `<a target="_top" href="${RUFFLE_ORIGIN}#downloads">Update Ruffle</a>`;
|
|
}
|
|
// 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 = `
|
|
<p>It appears you are running Ruffle on the "file:" protocol.</p>
|
|
<p>This doesn't work as browsers block many features from working for security reasons.</p>
|
|
<p>Instead, we invite you to setup a local server or either use the web demo or the desktop application.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="${RUFFLE_ORIGIN}/demo">Web Demo</a></li>
|
|
<li><a target="_top" href="${RUFFLE_ORIGIN}#downloads">Desktop Application</a></li>
|
|
`;
|
|
break;
|
|
case 4 /* PanicError.JavascriptConfiguration */:
|
|
// General error: Incorrect JavaScript configuration
|
|
errorBody = `
|
|
<p>Ruffle has encountered a major issue due to an incorrect JavaScript configuration.</p>
|
|
<p>If you are the server administrator, we invite you to check the error details to find out which parameter is at fault.</p>
|
|
<p>You can also consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#javascript-api">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 9 /* PanicError.WasmNotFound */:
|
|
// Self hosted: Cannot load `.wasm` file - file not found
|
|
errorBody = `
|
|
<p>Ruffle failed to load the required ".wasm" file component.</p>
|
|
<p>If you are the server administrator, please ensure the file has correctly been uploaded.</p>
|
|
<p>If the issue persists, you may need to use the "publicPath" setting: please consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configuration-options">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 8 /* PanicError.WasmMimeType */:
|
|
// Self hosted: Cannot load `.wasm` file - incorrect MIME type
|
|
errorBody = `
|
|
<p>Ruffle has encountered a major issue whilst trying to initialize.</p>
|
|
<p>This web server is not serving ".wasm" files with the correct MIME type.</p>
|
|
<p>If you are the server administrator, please consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-webassembly-mime-type">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 11 /* PanicError.SwfFetchError */:
|
|
errorBody = `
|
|
<p>Ruffle failed to load the Flash SWF file.</p>
|
|
<p>The most likely reason is that the file no longer exists, so there is nothing for Ruffle to load.</p>
|
|
<p>Try contacting the website administrator for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 12 /* PanicError.SwfCors */:
|
|
// Self hosted: Cannot load SWF file - CORS issues
|
|
errorBody = `
|
|
<p>Ruffle failed to load the Flash SWF file.</p>
|
|
<p>Access to fetch has likely been blocked by CORS policy.</p>
|
|
<p>If you are the server administrator, please consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-cors-header">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 6 /* PanicError.WasmCors */:
|
|
// Self hosted: Cannot load `.wasm` file - CORS issues
|
|
errorBody = `
|
|
<p>Ruffle failed to load the required ".wasm" file component.</p>
|
|
<p>Access to fetch has likely been blocked by CORS policy.</p>
|
|
<p>If you are the server administrator, please consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-cors-header">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 3 /* PanicError.InvalidWasm */:
|
|
// Self hosted: Cannot load `.wasm` file - incorrect configuration or missing files
|
|
errorBody = `
|
|
<p>Ruffle has encountered a major issue whilst trying to initialize.</p>
|
|
<p>It seems like this page has missing or invalid files for running Ruffle.</p>
|
|
<p>If you are the server administrator, please consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#addressing-a-compileerror">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 7 /* PanicError.WasmDownload */:
|
|
// Usually a transient network error or botched deployment
|
|
errorBody = `
|
|
<p>Ruffle has encountered a major issue whilst trying to initialize.</p>
|
|
<p>This can often resolve itself, so you can try reloading the page.</p>
|
|
<p>Otherwise, please contact the website administrator.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 10 /* PanicError.WasmDisabledMicrosoftEdge */:
|
|
// Self hosted: User has disabled WebAssembly in Microsoft Edge through the
|
|
// "Enhance your Security on the web" setting.
|
|
errorBody = `
|
|
<p>Ruffle failed to load the required ".wasm" file component.</p>
|
|
<p>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".</p>
|
|
<p>This will allow your browser to load the required ".wasm" files.</p>
|
|
<p>If the issue persists, you might have to use a different browser.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Frequently-Asked-Questions-For-Users#edge-webassembly-error">More Information</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 5 /* PanicError.JavascriptConflict */:
|
|
// Self hosted: Cannot load `.wasm` file - a native object / function is overriden
|
|
errorBody = `
|
|
<p>Ruffle has encountered a major issue whilst trying to initialize.</p>
|
|
<p>It seems like this page uses JavaScript code that conflicts with Ruffle.</p>
|
|
<p>If you are the server administrator, we invite you to try loading the file on a blank page.</p>
|
|
`;
|
|
if (isBuildOutdated) {
|
|
errorBody += `<p>You can also try to upload a more recent version of Ruffle that may circumvent the issue (current build is outdated: ${buildInfo.buildDate}).</p>`;
|
|
}
|
|
errorFooter = `
|
|
<li>${actionTag}</li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
case 1 /* PanicError.CSPConflict */:
|
|
// General error: Cannot load `.wasm` file - a native object / function is overriden
|
|
errorBody = `
|
|
<p>Ruffle has encountered a major issue whilst trying to initialize.</p>
|
|
<p>This web server's Content Security Policy does not allow the required ".wasm" component to run.</p>
|
|
<p>If you are the server administrator, please consult the Ruffle wiki for help.</p>
|
|
`;
|
|
errorFooter = `
|
|
<li><a target="_top" href="https://github.com/ruffle-rs/ruffle/wiki/Using-Ruffle#configure-wasm-csp">View Ruffle Wiki</a></li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
default:
|
|
// Unknown error
|
|
errorBody = `<p>Ruffle has encountered a major issue whilst trying to display this Flash content.</p>`;
|
|
if (!isBuildOutdated) {
|
|
errorBody += `<p>This isn't supposed to happen, so we'd really appreciate if you could file a bug!</p>`;
|
|
}
|
|
else {
|
|
errorBody += `<p>If you are the server administrator, please try to upload a more recent version of Ruffle (current build is outdated: ${buildInfo.buildDate}).</p>`;
|
|
}
|
|
errorFooter = `
|
|
<li>${actionTag}</li>
|
|
<li><a href="#" id="panic-view-details">View Error Details</a></li>
|
|
`;
|
|
break;
|
|
}
|
|
this.container.innerHTML = `
|
|
<div id="panic">
|
|
<div id="panic-title">Something went wrong :(</div>
|
|
<div id="panic-body">${errorBody}</div>
|
|
<div id="panic-footer">
|
|
<ul>${errorFooter}</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `<div class="message">
|
|
<p>Ruffle wasn't able to run the Flash embedded in this page.</p>
|
|
<p>You can try to open the file in a separate tab, to sidestep this issue.</p>
|
|
<div>
|
|
<a target="_blank" href="${this.swfUrl}">Open in a new tab</a>
|
|
</div>
|
|
</div>`;
|
|
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 = `<div class="message">
|
|
<p>The Ruffle emulator may not yet fully support all of ActionScript 3 used by this content.</p>
|
|
<p>Some parts of the content may not work as expected.</p>
|
|
<div>
|
|
<a target="_blank" class="more-info-link" href="https://github.com/ruffle-rs/ruffle/wiki/Frequently-Asked-Questions-For-Users">More info</a>
|
|
<button id="run-anyway-btn">Run anyway</button>
|
|
</div>
|
|
</div>`;
|
|
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 = `<div class="message">
|
|
<p>${message}</p>
|
|
<div>
|
|
<button id="continue-btn">continue</button>
|
|
</div>
|
|
</div>`;
|
|
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 <audio> or <video> node.
|
|
*/
|
|
function isFallbackElement(elem) {
|
|
let parent = elem.parentElement;
|
|
while (parent !== null) {
|
|
switch (parent.tagName) {
|
|
case "AUDIO":
|
|
case "VIDEO":
|
|
return true;
|
|
}
|
|
parent = parent.parentElement;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
;// CONCATENATED MODULE: ../core/dist/ruffle-embed.js
|
|
|
|
|
|
|
|
/**
|
|
* A polyfill html element.
|
|
*
|
|
* This specific class tries to polyfill existing `<embed>` tags,
|
|
* and should not be used. Prefer [[RufflePlayer]] instead.
|
|
*
|
|
* @internal
|
|
*/
|
|
class RuffleEmbed extends RufflePlayer {
|
|
/**
|
|
* Constructs a new Ruffle flash player for insertion onto the page.
|
|
*
|
|
* This specific class tries to polyfill existing `<embed>` tags,
|
|
* and should not be used. Prefer [[RufflePlayer]] instead.
|
|
*/
|
|
constructor() {
|
|
super();
|
|
}
|
|
/**
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
connectedCallback() {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
super.connectedCallback();
|
|
const src = this.attributes.getNamedItem("src");
|
|
if (src) {
|
|
const allowScriptAccess = (_b = (_a = this.attributes.getNamedItem("allowScriptAccess")) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null;
|
|
const menu = (_d = (_c = this.attributes.getNamedItem("menu")) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : null;
|
|
// Kick off the SWF download.
|
|
this.load({
|
|
url: src.value,
|
|
allowScriptAccess: isScriptAccessAllowed(allowScriptAccess, src.value),
|
|
parameters: (_f = (_e = this.attributes.getNamedItem("flashvars")) === null || _e === void 0 ? void 0 : _e.value) !== null && _f !== void 0 ? _f : null,
|
|
backgroundColor: (_h = (_g = this.attributes.getNamedItem("bgcolor")) === null || _g === void 0 ? void 0 : _g.value) !== null && _h !== void 0 ? _h : null,
|
|
base: (_k = (_j = this.attributes.getNamedItem("base")) === null || _j === void 0 ? void 0 : _j.value) !== null && _k !== void 0 ? _k : null,
|
|
menu: isBuiltInContextMenuVisible(menu),
|
|
salign: (_m = (_l = this.attributes.getNamedItem("salign")) === null || _l === void 0 ? void 0 : _l.value) !== null && _m !== void 0 ? _m : "",
|
|
quality: (_p = (_o = this.attributes.getNamedItem("quality")) === null || _o === void 0 ? void 0 : _o.value) !== null && _p !== void 0 ? _p : "high",
|
|
scale: (_r = (_q = this.attributes.getNamedItem("scale")) === null || _q === void 0 ? void 0 : _q.value) !== null && _r !== void 0 ? _r : "showAll",
|
|
wmode: (_t = (_s = this.attributes.getNamedItem("wmode")) === null || _s === void 0 ? void 0 : _s.value) !== null && _t !== void 0 ? _t : "window" /* WindowMode.Window */,
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Polyfill of HTMLObjectElement.
|
|
*
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
get src() {
|
|
var _a;
|
|
return (_a = this.attributes.getNamedItem("src")) === null || _a === void 0 ? void 0 : _a.value;
|
|
}
|
|
/**
|
|
* Polyfill of HTMLObjectElement.
|
|
*
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
set src(srcval) {
|
|
if (srcval) {
|
|
const attr = document.createAttribute("src");
|
|
attr.value = srcval;
|
|
this.attributes.setNamedItem(attr);
|
|
}
|
|
else {
|
|
this.attributes.removeNamedItem("src");
|
|
}
|
|
}
|
|
/**
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
static get observedAttributes() {
|
|
return ["src", "width", "height"];
|
|
}
|
|
/**
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
var _a, _b, _c, _d;
|
|
super.attributeChangedCallback(name, oldValue, newValue);
|
|
if (this.isConnected && name === "src") {
|
|
const src = this.attributes.getNamedItem("src");
|
|
if (src) {
|
|
this.load({
|
|
url: src.value,
|
|
parameters: (_b = (_a = this.attributes.getNamedItem("flashvars")) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : null,
|
|
base: (_d = (_c = this.attributes.getNamedItem("base")) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : null,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Checks if the given element may be polyfilled with this one.
|
|
*
|
|
* @param elem Element to check.
|
|
* @returns True if the element looks like a flash embed.
|
|
*/
|
|
static isInterdictable(elem) {
|
|
// Don't polyfill if the element is inside a specific node.
|
|
if (isFallbackElement(elem)) {
|
|
return false;
|
|
}
|
|
// Don't polyfill when no file is specified.
|
|
if (!elem.getAttribute("src")) {
|
|
return false;
|
|
}
|
|
// Don't polyfill when the file is a Youtube Flash source.
|
|
if (isYoutubeFlashSource(elem.getAttribute("src"))) {
|
|
// Workaround YouTube mixed content; this isn't what browsers do automatically, but while we're here, we may as well
|
|
workaroundYoutubeMixedContent(elem, "src");
|
|
return false;
|
|
}
|
|
// Check for MIME type.
|
|
const type = elem.getAttribute("type");
|
|
if (!type) {
|
|
// If no MIME type is specified, polyfill if movie is an SWF file.
|
|
return isSwfFilename(elem.getAttribute("src"));
|
|
}
|
|
else {
|
|
return isSwfMimeType(type);
|
|
}
|
|
}
|
|
/**
|
|
* Creates a RuffleEmbed that will polyfill and replace the given element.
|
|
*
|
|
* @param elem Element to replace.
|
|
* @returns Created RuffleEmbed.
|
|
*/
|
|
static fromNativeEmbedElement(elem) {
|
|
const externalName = registerElement("ruffle-embed", RuffleEmbed);
|
|
const ruffleObj = document.createElement(externalName);
|
|
ruffleObj.copyElement(elem);
|
|
return ruffleObj;
|
|
}
|
|
}
|
|
|
|
;// CONCATENATED MODULE: ../core/dist/ruffle-object.js
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Find and return the first value in obj with the given key.
|
|
* Many Flash params were case insensitive, so we use this when checking for them.
|
|
*
|
|
* @param obj Object to check
|
|
* @param key Key to find
|
|
* @param defaultValue Value if not found
|
|
* @returns Value if found, else [[defaultValue]]
|
|
*/
|
|
function findCaseInsensitive(obj, key, defaultValue) {
|
|
key = key.toLowerCase();
|
|
for (const [k, value] of Object.entries(obj)) {
|
|
if (k.toLowerCase() === key) {
|
|
return value;
|
|
}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
/**
|
|
* Returns all flash params ([[HTMLParamElement]]) that are for the given object.
|
|
*
|
|
* @param elem Element to check.
|
|
* @returns A record of every parameter.
|
|
*/
|
|
function paramsOf(elem) {
|
|
var _a, _b;
|
|
const params = {};
|
|
for (const param of elem.children) {
|
|
if (param instanceof HTMLParamElement) {
|
|
const key = (_a = param.attributes.getNamedItem("name")) === null || _a === void 0 ? void 0 : _a.value;
|
|
const value = (_b = param.attributes.getNamedItem("value")) === null || _b === void 0 ? void 0 : _b.value;
|
|
if (key && value) {
|
|
params[key] = value;
|
|
}
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
/**
|
|
* A polyfill html element.
|
|
*
|
|
* This specific class tries to polyfill existing `<object>` tags,
|
|
* and should not be used. Prefer [[RufflePlayer]] instead.
|
|
*
|
|
* @internal
|
|
*/
|
|
class RuffleObject extends RufflePlayer {
|
|
/**
|
|
* Constructs a new Ruffle flash player for insertion onto the page.
|
|
*
|
|
* This specific class tries to polyfill existing `<object>` tags,
|
|
* and should not be used. Prefer [[RufflePlayer]] instead.
|
|
*/
|
|
constructor() {
|
|
super();
|
|
this.params = {};
|
|
}
|
|
/**
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
connectedCallback() {
|
|
var _a;
|
|
super.connectedCallback();
|
|
this.params = paramsOf(this);
|
|
let url = null;
|
|
if (this.attributes.getNamedItem("data")) {
|
|
url = (_a = this.attributes.getNamedItem("data")) === null || _a === void 0 ? void 0 : _a.value;
|
|
}
|
|
else if (this.params["movie"]) {
|
|
url = this.params["movie"];
|
|
}
|
|
const allowScriptAccess = findCaseInsensitive(this.params, "allowScriptAccess", null);
|
|
const parameters = findCaseInsensitive(this.params, "flashvars", this.getAttribute("flashvars"));
|
|
const backgroundColor = findCaseInsensitive(this.params, "bgcolor", this.getAttribute("bgcolor"));
|
|
const base = findCaseInsensitive(this.params, "base", this.getAttribute("base"));
|
|
const menu = findCaseInsensitive(this.params, "menu", null);
|
|
const salign = findCaseInsensitive(this.params, "salign", "");
|
|
const quality = findCaseInsensitive(this.params, "quality", "high");
|
|
const scale = findCaseInsensitive(this.params, "scale", "showAll");
|
|
const wmode = findCaseInsensitive(this.params, "wmode", "window");
|
|
if (url) {
|
|
const options = { url };
|
|
options.allowScriptAccess = isScriptAccessAllowed(allowScriptAccess, url);
|
|
if (parameters) {
|
|
options.parameters = parameters;
|
|
}
|
|
if (backgroundColor) {
|
|
options.backgroundColor = backgroundColor;
|
|
}
|
|
if (base) {
|
|
options.base = base;
|
|
}
|
|
options.menu = isBuiltInContextMenuVisible(menu);
|
|
if (salign) {
|
|
options.salign = salign;
|
|
}
|
|
if (quality) {
|
|
options.quality = quality;
|
|
}
|
|
if (scale) {
|
|
options.scale = scale;
|
|
}
|
|
if (wmode) {
|
|
options.wmode = wmode;
|
|
}
|
|
// Kick off the SWF download.
|
|
this.load(options);
|
|
}
|
|
}
|
|
debugPlayerInfo() {
|
|
var _a;
|
|
let errorText = super.debugPlayerInfo();
|
|
errorText += "Player type: Object\n";
|
|
let url = null;
|
|
if (this.attributes.getNamedItem("data")) {
|
|
url = (_a = this.attributes.getNamedItem("data")) === null || _a === void 0 ? void 0 : _a.value;
|
|
}
|
|
else if (this.params["movie"]) {
|
|
url = this.params["movie"];
|
|
}
|
|
errorText += `SWF URL: ${url}\n`;
|
|
Object.keys(this.params).forEach((key) => {
|
|
errorText += `Param ${key}: ${this.params[key]}\n`;
|
|
});
|
|
Object.keys(this.attributes).forEach((key) => {
|
|
var _a;
|
|
errorText += `Attribute ${key}: ${(_a = this.attributes.getNamedItem(key)) === null || _a === void 0 ? void 0 : _a.value}\n`;
|
|
});
|
|
return errorText;
|
|
}
|
|
/**
|
|
* Polyfill of HTMLObjectElement.
|
|
*
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
get data() {
|
|
return this.getAttribute("data");
|
|
}
|
|
/**
|
|
* Polyfill of HTMLObjectElement.
|
|
*
|
|
* @ignore
|
|
* @internal
|
|
*/
|
|
set data(href) {
|
|
if (href) {
|
|
const attr = document.createAttribute("data");
|
|
attr.value = href;
|
|
this.attributes.setNamedItem(attr);
|
|
}
|
|
else {
|
|
this.attributes.removeNamedItem("data");
|
|
}
|
|
}
|
|
/**
|
|
* Checks if the given element may be polyfilled with this one.
|
|
*
|
|
* @param elem Element to check.
|
|
* @returns True if the element looks like a flash object.
|
|
*/
|
|
static isInterdictable(elem) {
|
|
var _a, _b;
|
|
// Don't polyfill if the element is inside a specific node.
|
|
if (isFallbackElement(elem)) {
|
|
return false;
|
|
}
|
|
// Don't polyfill if there's already a <ruffle-object> or a <ruffle-embed> inside the <object>.
|
|
if (elem.getElementsByTagName("ruffle-object").length > 0 ||
|
|
elem.getElementsByTagName("ruffle-embed").length > 0) {
|
|
return false;
|
|
}
|
|
// Don't polyfill if no movie specified.
|
|
const data = (_a = elem.attributes.getNamedItem("data")) === null || _a === void 0 ? void 0 : _a.value.toLowerCase();
|
|
const params = paramsOf(elem);
|
|
let isSwf;
|
|
// Check for SWF file.
|
|
if (data) {
|
|
// Don't polyfill when the file is a Youtube Flash source.
|
|
if (isYoutubeFlashSource(data)) {
|
|
// Workaround YouTube mixed content; this isn't what browsers do automatically, but while we're here, we may as well
|
|
workaroundYoutubeMixedContent(elem, "data");
|
|
return false;
|
|
}
|
|
isSwf = isSwfFilename(data);
|
|
}
|
|
else if (params && params["movie"]) {
|
|
// Don't polyfill when the file is a Youtube Flash source.
|
|
if (isYoutubeFlashSource(params["movie"])) {
|
|
// Workaround YouTube mixed content; this isn't what browsers do automatically, but while we're here, we may as well
|
|
const movie_elem = elem.querySelector("param[name='movie']");
|
|
if (movie_elem) {
|
|
workaroundYoutubeMixedContent(movie_elem, "value");
|
|
// The data attribute needs to be set for the re-fetch to happen
|
|
// It also needs to be set on Firefox for the YouTube object rewrite to work, regardless of mixed content
|
|
const movie_src = movie_elem.getAttribute("value");
|
|
if (movie_src) {
|
|
elem.setAttribute("data", movie_src);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
isSwf = isSwfFilename(params["movie"]);
|
|
}
|
|
else {
|
|
// Don't polyfill when no file is specified.
|
|
return false;
|
|
}
|
|
// Check ActiveX class ID.
|
|
const classid = (_b = elem.attributes
|
|
.getNamedItem("classid")) === null || _b === void 0 ? void 0 : _b.value.toLowerCase();
|
|
if (classid === FLASH_ACTIVEX_CLASSID.toLowerCase()) {
|
|
// classid is an old-IE style embed that would not work on modern browsers.
|
|
// Often there will be an <embed> inside the <object> that would take precedence.
|
|
// Only polyfill this <object> if it doesn't contain a polyfillable <embed> or
|
|
// another <object> that would be supported on modern browsers.
|
|
return (!Array.from(elem.getElementsByTagName("object")).some(RuffleObject.isInterdictable) &&
|
|
!Array.from(elem.getElementsByTagName("embed")).some(RuffleEmbed.isInterdictable));
|
|
}
|
|
else if (classid) {
|
|
// Non-Flash classid.
|
|
return false;
|
|
}
|
|
// Check for MIME type.
|
|
const type = elem.attributes.getNamedItem("type");
|
|
if (!type) {
|
|
// If no MIME type is specified, polyfill if movie is an SWF file.
|
|
return isSwf;
|
|
}
|
|
else {
|
|
return isSwfMimeType(type.value);
|
|
}
|
|
}
|
|
/**
|
|
* Creates a RuffleObject that will polyfill and replace the given element.
|
|
*
|
|
* @param elem Element to replace.
|
|
* @returns Created RuffleObject.
|
|
*/
|
|
static fromNativeObjectElement(elem) {
|
|
const externalName = registerElement("ruffle-object", RuffleObject);
|
|
const ruffleObj = (document.createElement(externalName));
|
|
// Avoid copying embeds-inside-objects to avoid double polyfilling.
|
|
for (const embedElem of Array.from(elem.getElementsByTagName("embed"))) {
|
|
if (RuffleEmbed.isInterdictable(embedElem)) {
|
|
embedElem.remove();
|
|
}
|
|
}
|
|
// Avoid copying objects-inside-objects to avoid double polyfilling.
|
|
// This may happen when Internet Explorer's conditional comments are used.
|
|
for (const objectElem of Array.from(elem.getElementsByTagName("object"))) {
|
|
if (RuffleObject.isInterdictable(objectElem)) {
|
|
objectElem.remove();
|
|
}
|
|
}
|
|
ruffleObj.copyElement(elem);
|
|
return ruffleObj;
|
|
}
|
|
}
|
|
|
|
;// CONCATENATED MODULE: ../core/dist/plugin-polyfill.js
|
|
|
|
/**
|
|
* Replacement object for `MimeTypeArray` that lets us install new fake mime
|
|
* types.
|
|
*
|
|
* Unlike plugins we can at least enumerate mime types in Firefox, so we don't
|
|
* lose data.
|
|
*
|
|
* We also expose a method called `install` which adds a new plugin. This is
|
|
* used to falsify Flash detection. If the existing `navigator.mimeTypes` has an
|
|
* `install` method, you should not use `RuffleMimeTypeArray` as some other
|
|
* plugin emulator is already present.
|
|
*/
|
|
class RuffleMimeTypeArray {
|
|
constructor(mimeTypes) {
|
|
this.__mimeTypes = [];
|
|
this.__namedMimeTypes = {};
|
|
if (mimeTypes) {
|
|
for (let i = 0; i < mimeTypes.length; i++) {
|
|
this.install(mimeTypes[i]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Install a MIME Type into the array.
|
|
*
|
|
* @param mimeType The mime type to install
|
|
*/
|
|
install(mimeType) {
|
|
const index = this.__mimeTypes.length;
|
|
this.__mimeTypes.push(mimeType);
|
|
this.__namedMimeTypes[mimeType.type] = mimeType;
|
|
this[mimeType.type] = mimeType;
|
|
this[index] = mimeType;
|
|
}
|
|
item(index) {
|
|
// This behavior is done to emulate a 32-bit uint,
|
|
// which browsers use.
|
|
return this.__mimeTypes[index >>> 0];
|
|
}
|
|
namedItem(name) {
|
|
return this.__namedMimeTypes[name];
|
|
}
|
|
get length() {
|
|
return this.__mimeTypes.length;
|
|
}
|
|
[Symbol.iterator]() {
|
|
return this.__mimeTypes[Symbol.iterator]();
|
|
}
|
|
}
|
|
/**
|
|
* Equivalent object to `Plugin` that allows us to falsify plugins.
|
|
*/
|
|
class RufflePlugin extends RuffleMimeTypeArray {
|
|
constructor(name, description, filename) {
|
|
super();
|
|
this.name = name;
|
|
this.description = description;
|
|
this.filename = filename;
|
|
}
|
|
}
|
|
/**
|
|
* Replacement object for `PluginArray` that lets us install new fake plugins.
|
|
*
|
|
* This object needs to wrap the native plugin array, since the user might have
|
|
* actual plugins installed. Firefox doesn't let us enumerate the array, though,
|
|
* which has some consequences. Namely, we can't actually perfectly wrap the
|
|
* native plugin array, at least unless there's some secret "unresolved object
|
|
* property name handler" that I've never known before in JS...
|
|
*
|
|
* We can still wrap `namedItem` perfectly at least.
|
|
*
|
|
* We also expose a method called `install` which adds a new plugin. This is
|
|
* used to falsify Flash detection. If the existing `navigator.plugins` has an
|
|
* `install` method, you should not use `RufflePluginArray` as some other plugin
|
|
* emulator is already present.
|
|
*/
|
|
class RufflePluginArray {
|
|
constructor(plugins) {
|
|
this.__plugins = [];
|
|
this.__namedPlugins = {};
|
|
for (let i = 0; i < plugins.length; i++) {
|
|
this.install(plugins[i]);
|
|
}
|
|
}
|
|
install(plugin) {
|
|
const index = this.__plugins.length;
|
|
this.__plugins.push(plugin);
|
|
this.__namedPlugins[plugin.name] = plugin;
|
|
this[plugin.name] = plugin;
|
|
this[index] = plugin;
|
|
}
|
|
item(index) {
|
|
// This behavior is done to emulate a 32-bit uint,
|
|
// which browsers use. Cloudflare's anti-bot
|
|
// checks rely on this.
|
|
return this.__plugins[index >>> 0];
|
|
}
|
|
namedItem(name) {
|
|
return this.__namedPlugins[name];
|
|
}
|
|
refresh() {
|
|
// Nothing to do, we just need to define the method.
|
|
}
|
|
[Symbol.iterator]() {
|
|
return this.__plugins[Symbol.iterator]();
|
|
}
|
|
get length() {
|
|
return this.__plugins.length;
|
|
}
|
|
}
|
|
/**
|
|
* A fake plugin designed to trigger Flash detection scripts.
|
|
*/
|
|
const FLASH_PLUGIN = new RufflePlugin("Shockwave Flash", "Shockwave Flash 32.0 r0", "ruffle.js");
|
|
/**
|
|
* A fake plugin designed to allow early detection of if the Ruffle extension is in use.
|
|
*/
|
|
const RUFFLE_EXTENSION = new RufflePlugin("Ruffle Extension", "Ruffle Extension", "ruffle.js");
|
|
FLASH_PLUGIN.install({
|
|
type: FUTURESPLASH_MIMETYPE,
|
|
description: "Shockwave Flash",
|
|
suffixes: "spl",
|
|
enabledPlugin: FLASH_PLUGIN,
|
|
});
|
|
FLASH_PLUGIN.install({
|
|
type: FLASH_MIMETYPE,
|
|
description: "Shockwave Flash",
|
|
suffixes: "swf",
|
|
enabledPlugin: FLASH_PLUGIN,
|
|
});
|
|
FLASH_PLUGIN.install({
|
|
type: FLASH7_AND_8_MIMETYPE,
|
|
description: "Shockwave Flash",
|
|
suffixes: "swf",
|
|
enabledPlugin: FLASH_PLUGIN,
|
|
});
|
|
FLASH_PLUGIN.install({
|
|
type: FLASH_MOVIE_MIMETYPE,
|
|
description: "Shockwave Flash",
|
|
suffixes: "swf",
|
|
enabledPlugin: FLASH_PLUGIN,
|
|
});
|
|
RUFFLE_EXTENSION.install({
|
|
type: "",
|
|
description: "Ruffle Detection",
|
|
suffixes: "",
|
|
enabledPlugin: RUFFLE_EXTENSION,
|
|
});
|
|
/**
|
|
* Install a fake plugin such that detectors will see it in `navigator.plugins`.
|
|
*
|
|
* This function takes care to check if the existing implementation of
|
|
* `navigator.plugins` already accepts fake plugin entries. If so, it will use
|
|
* that version of the plugin array. This allows the plugin polyfill to compose
|
|
* across multiple plugin emulators with the first emulator's polyfill winning.
|
|
*
|
|
* @param plugin The plugin to install
|
|
*/
|
|
function installPlugin(plugin) {
|
|
if (!("install" in navigator.plugins) || !navigator.plugins["install"]) {
|
|
Object.defineProperty(navigator, "plugins", {
|
|
value: new RufflePluginArray(navigator.plugins),
|
|
writable: false,
|
|
});
|
|
}
|
|
const plugins = navigator.plugins;
|
|
plugins.install(plugin);
|
|
if (plugin.length > 0 &&
|
|
(!("install" in navigator.mimeTypes) || !navigator.mimeTypes["install"])) {
|
|
Object.defineProperty(navigator, "mimeTypes", {
|
|
value: new RuffleMimeTypeArray(navigator.mimeTypes),
|
|
writable: false,
|
|
});
|
|
}
|
|
const mimeTypes = navigator.mimeTypes;
|
|
for (let i = 0; i < plugin.length; i += 1) {
|
|
mimeTypes.install(plugin[i]);
|
|
}
|
|
}
|
|
|
|
;// CONCATENATED MODULE: ../core/dist/polyfills.js
|
|
var _a, _b;
|
|
|
|
|
|
|
|
|
|
let isExtension;
|
|
const globalConfig = (_b = (_a = window.RufflePlayer) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {};
|
|
const jsScriptUrl = publicPath(globalConfig) + "ruffle.js";
|
|
/**
|
|
* Polyfill native Flash elements with Ruffle equivalents.
|
|
*
|
|
* This polyfill isn't fool-proof: If there's a chance site JavaScript has
|
|
* access to a pre-polyfill element, then this will break horribly. We can
|
|
* keep native objects out of the DOM, and thus out of JavaScript's grubby
|
|
* little hands, but only if we load first.
|
|
*/
|
|
let objects;
|
|
let polyfills_embeds;
|
|
/**
|
|
*
|
|
*/
|
|
function polyfillFlashInstances() {
|
|
try {
|
|
// Create live collections to track embed tags.
|
|
objects = objects !== null && objects !== void 0 ? objects : document.getElementsByTagName("object");
|
|
polyfills_embeds = polyfills_embeds !== null && polyfills_embeds !== void 0 ? polyfills_embeds : document.getElementsByTagName("embed");
|
|
// Replace <object> first, because <object> often wraps <embed>.
|
|
for (const elem of Array.from(objects)) {
|
|
if (RuffleObject.isInterdictable(elem)) {
|
|
const ruffleObject = RuffleObject.fromNativeObjectElement(elem);
|
|
ruffleObject.setIsExtension(isExtension);
|
|
elem.replaceWith(ruffleObject);
|
|
}
|
|
}
|
|
for (const elem of Array.from(polyfills_embeds)) {
|
|
if (RuffleEmbed.isInterdictable(elem)) {
|
|
const ruffleEmbed = RuffleEmbed.fromNativeEmbedElement(elem);
|
|
ruffleEmbed.setIsExtension(isExtension);
|
|
elem.replaceWith(ruffleEmbed);
|
|
}
|
|
}
|
|
}
|
|
catch (err) {
|
|
console.error(`Serious error encountered when polyfilling native Flash elements: ${err}`);
|
|
}
|
|
}
|
|
/**
|
|
* Inject Ruffle into <iframe> and <frame> elements.
|
|
*
|
|
* This polyfill isn't fool-proof either: On self-hosted builds, it may
|
|
* not work due to browsers CORS policy or be loaded too late for some
|
|
* libraries like SWFObject. These should be less of a problem on the
|
|
* web extension. This polyfill should, however, do the trick in most
|
|
* cases, but users should be aware of its natural limits.
|
|
*/
|
|
let iframes;
|
|
let polyfills_frames;
|
|
/**
|
|
*
|
|
*/
|
|
function polyfillFrames() {
|
|
// Create live collections to track embed tags.
|
|
iframes = iframes !== null && iframes !== void 0 ? iframes : document.getElementsByTagName("iframe");
|
|
polyfills_frames = polyfills_frames !== null && polyfills_frames !== void 0 ? polyfills_frames : document.getElementsByTagName("frame");
|
|
[iframes, polyfills_frames].forEach((elements) => {
|
|
for (const element of elements) {
|
|
if (element.dataset["rufflePolyfilled"] !== undefined) {
|
|
// Don't re-polyfill elements with the "data-ruffle-polyfilled" attribute.
|
|
continue;
|
|
}
|
|
element.dataset["rufflePolyfilled"] = "";
|
|
const elementWindow = element.contentWindow;
|
|
// Cross origin requests may reach an exception, so let's prepare for this eventuality.
|
|
const errorMessage = `Couldn't load Ruffle into ${element.tagName}[${element.src}]: `;
|
|
try {
|
|
if (elementWindow.document.readyState === "complete") {
|
|
injectRuffle(elementWindow, errorMessage);
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (!isExtension) {
|
|
// The web extension should be able to load Ruffle into cross origin frames
|
|
// because it has "all_frames" set to true in its manifest.json: RufflePlayer
|
|
// config won't be injected but it's not worth showing an error.
|
|
console.warn(errorMessage + err);
|
|
}
|
|
}
|
|
// Attach listener to the element to handle frame navigation.
|
|
element.addEventListener("load", () => {
|
|
injectRuffle(elementWindow, errorMessage);
|
|
}, false);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* @param elementWindow The (i)frame's window object.
|
|
* @param errorMessage The message to log when Ruffle cannot access the (i)frame's document.
|
|
*/
|
|
async function injectRuffle(elementWindow, errorMessage) {
|
|
var _a;
|
|
// The document is supposed to be completely loaded when this function is run.
|
|
// As Chrome may be unable to access the document properties, we have to delay the execution a little bit.
|
|
await new Promise((resolve) => {
|
|
window.setTimeout(() => {
|
|
resolve();
|
|
}, 100);
|
|
});
|
|
let elementDocument;
|
|
try {
|
|
elementDocument = elementWindow.document;
|
|
if (!elementDocument) {
|
|
// Don't polyfill if the window has no document: the element may have been removed from the parent window.
|
|
return;
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (!isExtension) {
|
|
console.warn(errorMessage + err);
|
|
}
|
|
return;
|
|
}
|
|
if (!isExtension &&
|
|
elementDocument.documentElement.dataset["ruffleOptout"] !== undefined) {
|
|
// Don't polyfill elements with the "data-ruffle-optout" attribute.
|
|
return;
|
|
}
|
|
if (!isExtension) {
|
|
if (!elementWindow.RufflePlayer) {
|
|
const script = elementDocument.createElement("script");
|
|
script.setAttribute("src", jsScriptUrl);
|
|
script.onload = () => {
|
|
// Inject parent configuration once the script is loaded, preventing it from being ignored.
|
|
elementWindow.RufflePlayer = {};
|
|
elementWindow.RufflePlayer.config = globalConfig;
|
|
};
|
|
elementDocument.head.appendChild(script);
|
|
}
|
|
}
|
|
else {
|
|
if (!elementWindow.RufflePlayer) {
|
|
elementWindow.RufflePlayer = {};
|
|
}
|
|
// Merge parent window and frame configurations, will likely be applied too late though.
|
|
elementWindow.RufflePlayer.config = Object.assign(Object.assign({}, globalConfig), ((_a = elementWindow.RufflePlayer.config) !== null && _a !== void 0 ? _a : {}));
|
|
}
|
|
}
|
|
/**
|
|
* Listen for changes to the DOM.
|
|
*/
|
|
function initMutationObserver() {
|
|
const observer = new MutationObserver(function (mutationsList) {
|
|
// If any nodes were added, re-run the polyfill to detect any new instances.
|
|
const nodesAdded = mutationsList.some((mutation) => mutation.addedNodes.length > 0);
|
|
if (nodesAdded) {
|
|
polyfillFlashInstances();
|
|
polyfillFrames();
|
|
}
|
|
});
|
|
observer.observe(document, { childList: true, subtree: true });
|
|
}
|
|
/**
|
|
* Polyfills the detection of Flash plugins in the browser.
|
|
*/
|
|
function pluginPolyfill() {
|
|
installPlugin(FLASH_PLUGIN);
|
|
}
|
|
/**
|
|
* Polyfills legacy Flash content on the page.
|
|
*
|
|
* @param isExt Whether or not Ruffle is running as a browser's extension.
|
|
*/
|
|
function polyfill(isExt) {
|
|
var _a;
|
|
isExtension = isExt;
|
|
const usingExtension = ((_a = navigator.plugins.namedItem("Ruffle Extension")) === null || _a === void 0 ? void 0 : _a.filename) ===
|
|
"ruffle.js";
|
|
if (isExtension || !usingExtension) {
|
|
polyfillFlashInstances();
|
|
polyfillFrames();
|
|
initMutationObserver();
|
|
}
|
|
}
|
|
|
|
;// CONCATENATED MODULE: ../core/dist/source-api.js
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Represents this particular version of Ruffle.
|
|
*
|
|
* Multiple APIs can be instantiated from different sources; e.g. an "extension"
|
|
* version, versus a "local" version. This expresses to the Public API
|
|
* negotiator (see [[PublicAPI]]) what this particular version of Ruffle is and
|
|
* how to control it.
|
|
*/
|
|
const SourceAPI = {
|
|
/**
|
|
* The version of this particular API, as a string in a semver compatible format.
|
|
*/
|
|
version: buildInfo.versionNumber,
|
|
/**
|
|
* Start up the polyfills.
|
|
*
|
|
* Do not run polyfills for more than one Ruffle source at a time.
|
|
*
|
|
* @param isExt Whether or not Ruffle is running as a browser's extension.
|
|
*/
|
|
polyfill(isExt) {
|
|
polyfill(isExt);
|
|
},
|
|
/**
|
|
* Polyfill the plugin detection.
|
|
*
|
|
* This needs to run before any plugin detection script does.
|
|
*/
|
|
pluginPolyfill() {
|
|
pluginPolyfill();
|
|
},
|
|
/**
|
|
* Create a Ruffle player element using this particular version of Ruffle.
|
|
*
|
|
* @returns The player element. This is a DOM element that may be inserted
|
|
* into the current page as you wish.
|
|
*/
|
|
createPlayer() {
|
|
const name = registerElement("ruffle-player", RufflePlayer);
|
|
return document.createElement(name);
|
|
},
|
|
};
|
|
|
|
;// CONCATENATED MODULE: ../core/dist/public-api.js
|
|
|
|
|
|
|
|
/**
|
|
* Represents the Ruffle public API.
|
|
*
|
|
* The public API exists primarily to allow multiple installs of Ruffle on a
|
|
* page (e.g. an extension install and a local one) to cooperate. In an ideal
|
|
* situation, all Ruffle sources on the page install themselves into a single
|
|
* public API, and then the public API picks the newest version by default.
|
|
*
|
|
* This API *is* versioned, in case we need to upgrade it. However, it must be
|
|
* backwards- and forwards-compatible with all known sources.
|
|
*/
|
|
class PublicAPI {
|
|
/**
|
|
* Construct the Ruffle public API.
|
|
*
|
|
* Do not use this function to negotiate a public API. Instead, use
|
|
* `public_api` to register your Ruffle source with an existing public API
|
|
* if it exists.
|
|
*
|
|
* Constructing a Public API will also trigger it to initialize Ruffle once
|
|
* the page loads, if the API has not already been superseded.
|
|
*
|
|
* @param prev What used to be in the public API slot.
|
|
*
|
|
* This is used to upgrade from a prior version of the public API, or from
|
|
* a user-defined configuration object placed in the public API slot.
|
|
*/
|
|
constructor(prev) {
|
|
this.sources = {};
|
|
this.config = {};
|
|
this.invoked = false;
|
|
this.newestName = null;
|
|
this.conflict = null;
|
|
if (prev !== undefined && prev !== null) {
|
|
if (prev instanceof PublicAPI) {
|
|
// We're upgrading from a previous API to a new one.
|
|
this.sources = prev.sources;
|
|
this.config = prev.config;
|
|
this.invoked = prev.invoked;
|
|
this.conflict = prev.conflict;
|
|
this.newestName = prev.newestName;
|
|
prev.superseded();
|
|
}
|
|
else if (prev.constructor === Object &&
|
|
prev["config"] instanceof Object) {
|
|
// We're the first, install user configuration.
|
|
this.config = prev["config"];
|
|
}
|
|
else {
|
|
// We're the first, but conflicting with someone else.
|
|
this.conflict = prev;
|
|
}
|
|
}
|
|
if (document.readyState === "loading") {
|
|
// Cloudflare Rocket Loader interferes with the DOMContentLoaded event,
|
|
// so we listen for readystatechange instead
|
|
document.addEventListener("readystatechange", this.init.bind(this));
|
|
}
|
|
else {
|
|
window.setTimeout(this.init.bind(this), 0);
|
|
}
|
|
}
|
|
/**
|
|
* The version of the public API.
|
|
*
|
|
* This is *not* the version of Ruffle itself.
|
|
*
|
|
* This allows a page with an old version of the Public API to be upgraded
|
|
* to a new version of the API. The public API is intended to be changed
|
|
* very infrequently, if at all, but this provides an escape mechanism for
|
|
* newer Ruffle sources to upgrade older installations.
|
|
*
|
|
* @returns The version of this public API.
|
|
*/
|
|
get version() {
|
|
return "0.1.0";
|
|
}
|
|
/**
|
|
* Register a given source with the Ruffle Public API.
|
|
*
|
|
* @param name The name of the source.
|
|
*/
|
|
registerSource(name) {
|
|
this.sources[name] = SourceAPI;
|
|
}
|
|
/**
|
|
* Determine the name of the newest registered source in the Public API.
|
|
*
|
|
* @returns The name of the source, or `null` if no source
|
|
* has yet to be registered.
|
|
*/
|
|
newestSourceName() {
|
|
let newestName = null, newestVersion = Version.fromSemver("0.0.0");
|
|
for (const k in this.sources) {
|
|
if (Object.prototype.hasOwnProperty.call(this.sources, k)) {
|
|
const kVersion = Version.fromSemver(this.sources[k].version);
|
|
if (kVersion.hasPrecedenceOver(newestVersion)) {
|
|
newestName = k;
|
|
newestVersion = kVersion;
|
|
}
|
|
}
|
|
}
|
|
return newestName;
|
|
}
|
|
/**
|
|
* Negotiate and start Ruffle.
|
|
*
|
|
* This function reads the config parameter to determine which polyfills
|
|
* should be enabled. If the configuration parameter is missing, then we
|
|
* use a built-in set of defaults sufficient to fool sites with static
|
|
* content and weak plugin detection.
|
|
*/
|
|
init() {
|
|
if (!this.invoked) {
|
|
this.invoked = true;
|
|
this.newestName = this.newestSourceName();
|
|
if (this.newestName === null) {
|
|
throw new Error("No registered Ruffle source!");
|
|
}
|
|
const polyfills = "polyfills" in this.config ? this.config.polyfills : true;
|
|
if (polyfills !== false) {
|
|
this.sources[this.newestName].polyfill(this.newestName === "extension");
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Look up the newest Ruffle source and return it's API.
|
|
*
|
|
* @returns An instance of the Source API.
|
|
*/
|
|
newest() {
|
|
const name = this.newestSourceName();
|
|
return name !== null ? this.sources[name] : null;
|
|
}
|
|
/**
|
|
* Look up a specific Ruffle version (or any version satisfying a given set
|
|
* of requirements) and return it's API.
|
|
*
|
|
* @param ver_requirement A set of semantic version requirement
|
|
* strings that the player version must satisfy.
|
|
* @returns An instance of the Source API, if one or more
|
|
* sources satisfied the requirement.
|
|
*/
|
|
satisfying(ver_requirement) {
|
|
const requirement = VersionRange.fromRequirementString(ver_requirement);
|
|
let valid = null;
|
|
for (const k in this.sources) {
|
|
if (Object.prototype.hasOwnProperty.call(this.sources, k)) {
|
|
const version = Version.fromSemver(this.sources[k].version);
|
|
if (requirement.satisfiedBy(version)) {
|
|
valid = this.sources[k];
|
|
}
|
|
}
|
|
}
|
|
return valid;
|
|
}
|
|
/**
|
|
* Look up the newest Ruffle version compatible with the `local` source, if
|
|
* it's installed. Otherwise, use the latest version.
|
|
*
|
|
* @returns An instance of the Source API
|
|
*/
|
|
localCompatible() {
|
|
if (this.sources["local"] !== undefined) {
|
|
return this.satisfying("^" + this.sources["local"].version);
|
|
}
|
|
else {
|
|
return this.newest();
|
|
}
|
|
}
|
|
/**
|
|
* Look up the newest Ruffle version with the exact same version as the
|
|
* `local` source, if it's installed. Otherwise, use the latest version.
|
|
*
|
|
* @returns An instance of the Source API
|
|
*/
|
|
local() {
|
|
if (this.sources["local"] !== undefined) {
|
|
return this.satisfying("=" + this.sources["local"].version);
|
|
}
|
|
else {
|
|
return this.newest();
|
|
}
|
|
}
|
|
/**
|
|
* Indicates that this version of the public API has been superseded by a
|
|
* newer version.
|
|
*
|
|
* This should only be called by a newer version of the Public API.
|
|
* Identical versions of the Public API should not supersede older versions
|
|
* of that same API.
|
|
*
|
|
* Unfortunately, we can't disable polyfills after-the-fact, so this
|
|
* only lets you disable the init event...
|
|
*/
|
|
superseded() {
|
|
this.invoked = true;
|
|
}
|
|
/**
|
|
* Join a source into the public API, if it doesn't already exist.
|
|
*
|
|
* @param prevRuffle The previous iteration of the Ruffle API.
|
|
*
|
|
* The `prevRuffle` param lists the previous object in the RufflePlayer
|
|
* slot. We perform some checks to see if this is a Ruffle public API or a
|
|
* conflicting object. If this is conflicting, then a new public API will
|
|
* be constructed (see the constructor information for what happens to
|
|
* `prevRuffle`).
|
|
*
|
|
* Note that Public API upgrades are deliberately not enabled in this
|
|
* version of Ruffle, since there is no Public API to upgrade from.
|
|
* @param sourceName The name of this particular
|
|
* Ruffle source.
|
|
*
|
|
* If both parameters are provided they will be used to define a new Ruffle
|
|
* source to register with the public API.
|
|
* @returns The Ruffle Public API.
|
|
*/
|
|
static negotiate(prevRuffle, sourceName) {
|
|
let publicAPI;
|
|
if (prevRuffle instanceof PublicAPI) {
|
|
publicAPI = prevRuffle;
|
|
}
|
|
else {
|
|
publicAPI = new PublicAPI(prevRuffle);
|
|
}
|
|
if (sourceName !== undefined) {
|
|
publicAPI.registerSource(sourceName);
|
|
// Install the faux plugin detection immediately.
|
|
// This is necessary because scripts such as SWFObject check for the
|
|
// Flash Player immediately when they load.
|
|
// TODO: Maybe there's a better place for this.
|
|
const polyfills = "polyfills" in publicAPI.config
|
|
? publicAPI.config.polyfills
|
|
: true;
|
|
if (polyfills !== false) {
|
|
SourceAPI.pluginPolyfill();
|
|
}
|
|
}
|
|
return publicAPI;
|
|
}
|
|
}
|
|
|
|
;// CONCATENATED MODULE: ./src/ruffle.ts
|
|
|
|
function handleMessage(message) {
|
|
var _a;
|
|
switch (message.type) {
|
|
case "load": {
|
|
const api = (_a = window.RufflePlayer) !== null && _a !== void 0 ? _a : {};
|
|
api.config = Object.assign(Object.assign({}, message.config), api.config);
|
|
window.RufflePlayer = PublicAPI.negotiate(api, "extension");
|
|
return {};
|
|
}
|
|
case "ping":
|
|
// Ping back.
|
|
return {};
|
|
default:
|
|
// Ignore unknown messages.
|
|
return null;
|
|
}
|
|
}
|
|
let ID = null;
|
|
if (document.currentScript !== undefined &&
|
|
document.currentScript !== null &&
|
|
"src" in document.currentScript &&
|
|
document.currentScript.src !== "") {
|
|
try {
|
|
ID = new URL(document.currentScript.src).searchParams.get("id");
|
|
}
|
|
catch (_) {
|
|
// ID remains null.
|
|
}
|
|
}
|
|
if (ID) {
|
|
window.addEventListener("message", (event) => {
|
|
// We only accept messages from ourselves.
|
|
if (event.source !== window) {
|
|
return;
|
|
}
|
|
const { to, index, data } = event.data;
|
|
if (to === `ruffle_page${ID}`) {
|
|
const response = handleMessage(data);
|
|
if (response) {
|
|
const message = {
|
|
to: `ruffle_content${ID}`,
|
|
index,
|
|
data: response,
|
|
};
|
|
window.postMessage(message, "*");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
})();
|
|
|
|
/******/ })()
|
|
; |