eaglercraft-1.8/sources/wasm-gc-teavm-bootstrap/js/main.js

378 lines
14 KiB
JavaScript
Raw Normal View History

/*
* Copyright (c) 2024 lax1dude. All Rights Reserved.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
/**
* @param {*} msg
*/
function logInfo(msg) {
console.log("LoaderBootstrap: [INFO] " + msg);
}
/**
* @param {*} msg
*/
function logWarn(msg) {
console.log("LoaderBootstrap: [WARN] " + msg);
}
/**
* @param {*} msg
*/
function logError(msg) {
console.error("LoaderBootstrap: [ERROR] " + msg);
}
/** @type {function(string,number):ArrayBuffer|null} */
var decodeBase64Impl = null;
/**
* @return {function(string,number):ArrayBuffer}
*/
function createBase64Decoder() {
const revLookup = [];
const code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (var i = 0, len = code.length; i < len; ++i) {
revLookup[code.charCodeAt(i)] = i;
}
revLookup["-".charCodeAt(0)] = 62;
revLookup["_".charCodeAt(0)] = 63;
/**
* @param {string} b64
* @param {number} start
* @return {!Array<number>}
*/
function getLens(b64, start) {
const len = b64.length - start;
if (len % 4 > 0) {
throw new Error("Invalid string. Length must be a multiple of 4");
}
var validLen = b64.indexOf("=", start);
if (validLen === -1) {
validLen = len;
}else {
validLen -= start;
}
const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4);
return [validLen, placeHoldersLen];
}
/**
* @param {string} b64
* @param {number} start
* @return {ArrayBuffer}
*/
function decodeImpl(b64, start) {
var tmp;
const lens = getLens(b64, start);
const validLen = lens[0];
const placeHoldersLen = lens[1];
const arr = new Uint8Array(((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen);
var curByte = 0;
const len = (placeHoldersLen > 0 ? validLen - 4 : validLen) + start;
var i;
for (i = start; i < len; i += 4) {
tmp = (revLookup[b64.charCodeAt(i)] << 18) |
(revLookup[b64.charCodeAt(i + 1)] << 12) |
(revLookup[b64.charCodeAt(i + 2)] << 6) |
revLookup[b64.charCodeAt(i + 3)]
arr[curByte++] = (tmp >> 16) & 0xFF
arr[curByte++] = (tmp >> 8) & 0xFF
arr[curByte++] = tmp & 0xFF
}
if (placeHoldersLen === 2) {
tmp = (revLookup[b64.charCodeAt(i)] << 2) |
(revLookup[b64.charCodeAt(i + 1)] >> 4)
arr[curByte++] = tmp & 0xFF
}else if (placeHoldersLen === 1) {
tmp = (revLookup[b64.charCodeAt(i)] << 10) |
(revLookup[b64.charCodeAt(i + 1)] << 4) |
(revLookup[b64.charCodeAt(i + 2)] >> 2)
arr[curByte++] = (tmp >> 8) & 0xFF
arr[curByte++] = tmp & 0xFF
}
return arr.buffer;
}
return decodeImpl;
}
/**
* @param {string} url
* @param {number} start
* @return {ArrayBuffer}
*/
function decodeBase64(url, start) {
if(!decodeBase64Impl) {
decodeBase64Impl = createBase64Decoder();
}
return decodeBase64Impl(url, start);
}
/**
* @param {number} ms
* @return {!Promise}
*/
function asyncSleep(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
}
/**
* @param {string} url
* @return {!Promise<ArrayBuffer>}
*/
function downloadURL(url) {
return new Promise(function(resolve) {
fetch(url, { "cache": "force-cache" })
.then(function(res) {
return res.arrayBuffer();
})
.then(resolve)
.catch(function(ex) {
logError("Failed to fetch URL! " + ex);
resolve(null);
});
});
}
/**
* @param {string} url
* @return {!Promise<ArrayBuffer>}
*/
function downloadDataURL(url) {
if(!url.startsWith("data:application/octet-stream;base64,")) {
return downloadURL(url);
}else {
return new Promise(function(resolve) {
downloadURL(url).then(function(res) {
if(res) {
resolve(res);
}else {
logWarn("Failed to decode base64 via fetch, doing it the slow way instead...");
try {
resolve(decodeBase64(url, 37));
}catch(ex) {
logError("Failed to decode base64! " + ex);
resolve(null);
}
}
});
});
}
}
/**
* @param {HTMLElement} rootElement
* @param {string} msg
*/
function displayInvalidEPW(rootElement, msg) {
const downloadFailureMsg = /** @type {HTMLElement} */ (document.createElement("h2"));
downloadFailureMsg.style.color = "#AA0000";
downloadFailureMsg.style.padding = "25px";
downloadFailureMsg.style.fontFamily = "sans-serif";
downloadFailureMsg.style["marginBlock"] = "0px";
downloadFailureMsg.appendChild(document.createTextNode(msg));
rootElement.appendChild(downloadFailureMsg);
const downloadFailureMsg2 = /** @type {HTMLElement} */ (document.createElement("h4"));
downloadFailureMsg2.style.color = "#AA0000";
downloadFailureMsg2.style.padding = "25px";
downloadFailureMsg2.style.fontFamily = "sans-serif";
downloadFailureMsg2.style["marginBlock"] = "0px";
downloadFailureMsg2.appendChild(document.createTextNode("Try again later"));
rootElement.style.backgroundColor = "white";
rootElement.appendChild(downloadFailureMsg2);
}
window.main = async function() {
if(typeof window.eaglercraftXOpts === "undefined") {
const msg = "window.eaglercraftXOpts is not defined!";
logError(msg);
alert(msg);
return;
}
const containerId = window.eaglercraftXOpts.container;
if(typeof containerId !== "string") {
const msg = "window.eaglercraftXOpts.container is not a string!";
logError(msg);
alert(msg);
return;
}
var assetsURI = window.eaglercraftXOpts.assetsURI;
if(typeof assetsURI !== "string") {
if((typeof assetsURI === "object") && (typeof assetsURI[0] === "object") && (typeof assetsURI[0]["url"] === "string")) {
assetsURI = assetsURI[0]["url"];
}else {
const msg = "window.eaglercraftXOpts.assetsURI is not a string!";
logError(msg);
alert(msg);
return;
}
}
const rootElement = /** @type {HTMLElement} */ (document.getElementById(containerId));
if(!rootElement) {
const msg = "window.eaglercraftXOpts.container \"" + containerId + "\" is not a known element id!";
logError(msg);
alert(msg);
return;
}
var node;
while(node = rootElement.lastChild) {
rootElement.removeChild(node);
}
const splashElement = /** @type {HTMLElement} */ (document.createElement("div"));
splashElement.style.width = "100%";
splashElement.style.height = "100%";
splashElement.style.setProperty("image-rendering", "pixelated");
splashElement.style.background = "center / contain no-repeat url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAAAAAB3tzPbAAAACXBIWXMAAC4jAAAuIwF4pT92AAAG+UlEQVR42u2cy23jOhRATwbTwGwFvAJoF6BFGjColcGkASNuIPA6C68DN+BADZiCVxLSQBYqIGYBAbSdEvwWkvUzZWfymwlwCQwQUZeXPOT9URPkYs/3bj8QAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAH4x9vPvzFpAhAzM98UILmqfjDf1YT0N/cBk+71v+wDSczHmDeJ6TqO+SIfyD7IvC9g33Yc7dP6CQDxB+q62Hc2xnyJD2Sf5vuzL3Hi5MM0WbCN51u/Y/30ryEGmDVHlhwsY9Y7xlq0CuzVc4lh2n7NkGsnQ1nB7IefmrY/araJcbrq6Ryk9YqW4l3J/dHww1jdej+8kte042EW0Nba1hyWdl+9irq/FNXaD6BbQoexuvf+tQC2vX1+AFvP0kxiuyidfWwEbOtQtK0n0r6xbYCKsLcM21+pLZX3u4984Kq2xlnWDimllRudAXEpkGSHfqMzsmxfWnLWNf9aQznW4wMZWOMJxvGs/Ff5X+yPcD0g3dqZesdsI2f7Z2/73W2JSok9Gqu7P1q/I2qtj0qn/ZkTaCPWO2a0VyjrxY7sNUG1LxRlaE90MpDpGVeAxpaGobN2XPWH0aQVE1stfXPAj0+XzUmcob3aTRdVZ2+tRv+gMNBDaTkZ4k6uhtYPaK7iUkUcx9lgij92gZ6aXmxoDeK8D1hPfm18oBvTfPGwXoVG+4VfXcwl8dEOtCJS7De9M0VTqTA2p081O3kJ+uk5cU/RVN8C262Ms9HMlLHSmhNFTcc9u1uQRX4jMhqyNIk1GRk69a6hb0IDZ3pITnbfNqFuJWE9gbYrfmSqen/SiKy27G0VS20VWc+UEn59/YDPkc+0EunrAXQ/JXucYL+3VutyAqvP5wFvtEoyQPsMJMpKc3v7/Su9ALLkhAJDPCObGTDmonfNHAij3sg5866fmTHGnFt/crroh6vEv/Rq6vhEoP7hWWb2ylSQZP5zOVrDqVxSZnm/xL6OFnZwF3/4JoyGjyXu1X3n0rEFyE5Jzc5KEDfT7s2ZYs52s5e1HU88hB17nKTqAroXWPpXiHbN7R3Q8fVDbjzU6vb8hUbX67FWN8Xo4U5SIWjbukr1knY9XrcwS30aOuTatqa0vkA6cI05dyPrzWBbj7ZZrPUT2O7pdpKFtp4rph0E0AxtfN0u9kNVg25d4BPiDF0+R83dPol7/l4m4yQmQzdX+ISewqTnc8ngp94yaCan4vT+Hc228q8/T35+e8+XueSqCaPmEz9ofdbX6eSqE5iN/m4A8Qd9w/1bAEl2fPmafT3Axdv/ytlFeXUwTZyyf+NA3hWDGPrm+HXtHSdQ7nrz7fvv+MPFe/9Q3nAS+iYA3zcKCYAACIAACIAACIAACIAACIAACIAA1C2Komh++r9cogdv90M0+GoZAVHkSiGSaFmOmJdTRdESiKJ5Je4eovnSldoGNJ44gTBNbx+XH7tDYxwOniAPgEdygGWxTm/jBCAHV0u7xa90PV64IW0uOWdCapK7t600vfF2j4Ad5FCE4IopCSWMSg0Q4NgRVNKrwIBJ1ZDGxXO/5+fxhDvFQ87EsHxZMy9Sli/raMbjf9eqMpiciQG3yYOJwW1eQoBoesNBzG3yKdvqNwie1HMwiXFcwo7L7aMBtlSrC7c79RzyUm5w0f66Gk1vcJs8vFYHxUvy/u8leJz4N8t8vX5ccl04Chz5BOLR+mVVWXX5lsU4ncSOFevL7WFsJbYiPfQpcvJwhNsBxKiwcHDPNnoojzp8Jh8PnusiSMcLd1B8R5i+Igq5/BZKU3IEO8cIpoqw6L5NR8kjuOIaFR6GlmKdvmnhuFTsfqNwTBnzBOo+ZFua+jh3jAZtnksMu/b850wIfh1sVwVPhMEzKK9lz/+7Hi3Kx8CjOajVbVCEz3kIT1wyYnsD6s5t8tUaGLFpTfC7q2TH4rjzHMCoGgqTOJiMFi/TY5kduOJWHfzdtzdFrS4PYBwzhi0LAKcAdTcvKhur+VWQ3/TWcq/+LJG5VahUsILHUDGiGCmKy26cOrxlxwZUsMHlvVDW7lMQwghGOGZpmt6zcdFD47EhtQVyWySQRHUgVDzhmkeClyZFlGmiA5BH0WpyB+twPp/cgQpQBH0Lqt6qaTwfs+OW6Kl/RrdET/WqQi5BgWLDqNxmdV/Mo1X1QX5Ms0Pq/jmaP7d2/b6IVq3HW+a9qT7v6/TDNv2+tVA0hzz8klroc07AbXKmN98YQMppARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARCAD2//A2iD9ZsgY5XpAAAAAElFTkSuQmCC\") white";
rootElement.appendChild(splashElement);
/** @type {ArrayBuffer} */
var theEPWFileBuffer;
if(assetsURI.startsWith("data:")) {
logInfo("Downloading EPW file \"<data: " + assetsURI.length + " chars>\"...");
theEPWFileBuffer = await downloadDataURL(assetsURI);
}else {
logInfo("Downloading EPW file \"" + assetsURI + "\"...");
theEPWFileBuffer = await downloadURL(assetsURI);
}
var isInvalid = false;
if(!theEPWFileBuffer) {
isInvalid = true;
}else if(theEPWFileBuffer.byteLength < 384) {
logError("The EPW file is too short");
isInvalid = true;
}
if(isInvalid) {
rootElement.removeChild(splashElement);
const msg = "Failed to download EPW file!";
displayInvalidEPW(rootElement, msg);
logError(msg);
return;
}
const dataView = new DataView(theEPWFileBuffer);
if(dataView.getUint32(0, true) !== 608649541 || dataView.getUint32(4, true) !== 1297301847) {
logError("The file is not an EPW file");
isInvalid = true;
}
const phileLength = theEPWFileBuffer.byteLength;
if(dataView.getUint32(8, true) !== phileLength) {
logError("The EPW file is the wrong length");
isInvalid = true;
}
if(isInvalid) {
rootElement.removeChild(splashElement);
const msg = "EPW file is invalid!";
displayInvalidEPW(rootElement, msg);
logError(msg);
return;
}
const textDecoder = new TextDecoder("utf-8");
const splashDataOffset = dataView.getUint32(100, true);
const splashDataLength = dataView.getUint32(104, true);
const splashMIMEOffset = dataView.getUint32(108, true);
const splashMIMELength = dataView.getUint32(112, true);
if(splashDataOffset < 0 || splashDataOffset + splashDataLength > phileLength
|| splashMIMEOffset < 0 || splashMIMEOffset + splashMIMELength > phileLength) {
logError("The EPW file contains an invalid offset (component: splash)");
isInvalid = true;
}
if(isInvalid) {
rootElement.removeChild(splashElement);
const msg = "EPW file is invalid!";
displayInvalidEPW(rootElement, msg);
logError(msg);
return;
}
const splashBinSlice = new Uint8Array(theEPWFileBuffer, splashDataOffset, splashDataLength);
const splashMIMESlice = new Uint8Array(theEPWFileBuffer, splashMIMEOffset, splashMIMELength);
const splashURL = URL.createObjectURL(new Blob([ splashBinSlice ], { "type": textDecoder.decode(splashMIMESlice) }));
logInfo("Loaded splash img: " + splashURL);
splashElement.style.background = "center / contain no-repeat url(\"" + splashURL + "\"), 0px 0px / 1000000% 1000000% no-repeat url(\"" + splashURL + "\") white";
// allow the screen to update
await asyncSleep(20);
const loaderJSOffset = dataView.getUint32(164, true);
const loaderJSLength = dataView.getUint32(168, true);
const loaderWASMOffset = dataView.getUint32(180, true);
const loaderWASMLength = dataView.getUint32(184, true);
if(loaderJSOffset < 0 || loaderJSOffset + loaderJSLength > phileLength
|| loaderWASMOffset < 0 || loaderWASMOffset + loaderWASMLength > phileLength) {
logError("The EPW file contains an invalid offset (component: loader)");
isInvalid = true;
}
if(isInvalid) {
rootElement.removeChild(splashElement);
const msg = "EPW file is invalid!";
displayInvalidEPW(rootElement, msg);
logError(msg);
return;
}
const loaderJSSlice = new Uint8Array(theEPWFileBuffer, loaderJSOffset, loaderJSLength);
const loaderJSURL = URL.createObjectURL(new Blob([ loaderJSSlice ], { "type": "text/javascript;charset=utf-8" }));
logInfo("Loaded loader.js: " + splashURL);
const loaderWASMSlice = new Uint8Array(theEPWFileBuffer, loaderWASMOffset, loaderWASMLength);
const loaderWASMURL = URL.createObjectURL(new Blob([ loaderWASMSlice ], { "type": "application/wasm" }));
logInfo("Loaded loader.wasm: " + loaderWASMURL);
const optsObj = {};
for(const [key, value] of Object.entries(window.eaglercraftXOpts)) {
if(key !== "container" && key !== "assetsURI") {
optsObj[key] = value;
}
}
window.__eaglercraftXLoaderContextPre = {
"rootElement": rootElement,
"eaglercraftXOpts": optsObj,
"theEPWFileBuffer": theEPWFileBuffer,
"loaderWASMURL": loaderWASMURL,
"splashURL": splashURL
};
logInfo("Appending loader.js to document...");
const scriptElement = /** @type {HTMLScriptElement} */ (document.createElement("script"));
scriptElement.type = "text/javascript";
scriptElement.src = loaderJSURL;
document.head.appendChild(scriptElement);
};