';
}
// query string into object
var queries = {};
for (var i = 0; i < search.length; i++) {
var p = search[i].split("=");
queries[p[0]] = p[1];
}
// Binary to UTF-8
function u8atoutf8(data) {
return new TextDecoder().decode(data);
}
function avShift(array, shift) {
for (var i = 0; i < array.length; i++) {
array[i] += shift;
}
return array;
}
// date time
function getTime() {
var dateTime = new Date();
return dateTime.getFullYear().toString()+"-"+(dateTime.getMonth()+1).toString()+"-"+dateTime.getDate().toString()+"-"+dateTime.getHours().toString()+"-"+dateTime.getMinutes().toString();
}
// bytes to human-readable string
function bytesToHumanReadable(bytes) {
bytes = bytes || 0;
var extension = -1;
while (bytes >= 1000) {
bytes /= 1000;
extension++;
}
return bytes.toFixed(2) + " " + "KMGTPEZY".charAt(extension) + "B";
}
// js has no built-in capitalization function
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// key press stuff
function fakeKey(type, info) {
var e = new KeyboardEvent(type, {code: info.code || undefined, key: info.key || undefined, shiftKey: info.shiftKey || undefined});
document.dispatchEvent(e);
}
function fakeKeyPress(info) {
fakeKey("keydown", info);
window.setTimeout(function() {
fakeKey("keyup", info);
}, 50);
}
function fakeCharPress(key) {
if (charToCodeMap.hasOwnProperty(key)) fakeKeyPress({code: charToCodeMap[key].code, key: charToKeyMap.hasOwnProperty(key) ? charToKeyMap[key].key : key, shiftKey: charToCodeMap[key].hasOwnProperty("shift") ? true : false});
}
function sendText(text) {
for (var i = 0; i < text.length; i++) {
fakeCharPress(text.charAt(i));
}
}
// indexedDB
function openIdb() {
var request = indexedDB.open("webretro", 1);
request.onsuccess = function(e) {
wIdb = e.target.result;
}
request.onupgradeneeded = function(e) {
var store = e.target.result.createObjectStore("main", {keyPath: "key"});
store.transaction.oncomplete = function(e2) {
wIdb = e.target.db;
}
}
}
openIdb();
function setIdbItem(key, value) {
wIdb.transaction("main", "readwrite").objectStore("main").put({key: key, value: value});
}
function getIdbItem(key) {
return new Promise(function(resolve) {
wIdb.transaction("main", "readwrite").objectStore("main").get(key).onsuccess = function(e) {
resolve(e.target.result ? e.target.result.value : null);
}
});
}
function getAllIdbItems() {
return new Promise(function(resolve) {
wIdb.transaction("main", "readwrite").objectStore("main").getAll().onsuccess = function(e) {
resolve(e.target.result ? e.target.result : null);
}
});
}
function removeIdbItem(key) {
wIdb.transaction("main", "readwrite").objectStore("main").delete(key);
}
// localStorage to indexedDB
async function tryLsToIdb() {
var ls = Object.keys(window.localStorage);
for (var i = 0; i < ls.length; i++) {
if (ls[i].startsWith("RetroArch_saves_")) {
setIdbItem(ls[i], new Uint8Array(JSON.parse(window.localStorage.getItem(ls[i]))));
window.localStorage.removeItem(ls[i]);
}
if (i == ls.length - 1) return;
}
}
// side alerts
function sideAlert(initialText, time) {
var p = document.createElement("p");
p.className = "sidealert";
p.appendChild(document.createTextNode(initialText));
sideAlertHolder.appendChild(p);
window.setTimeout(function() {
p.classList.add("on");
}, 10);
this.dismiss = function() {
p.classList.remove("on");
window.setTimeout(function() {
p.remove();
}, 100);
}
this.setText = function(text) {
p.textContent = text;
}
if (time) window.setTimeout(this.dismiss, time);
}
// change background for status messages
function setStatus(message) {
loadStatus = message;
canvas.style.backgroundImage = 'url("data:image/svg+xml;base64,' + btoa('') + '")';
}
// remove status messages
function removeStatus(message) {
if (loadStatus === message) setStatus("");
}
// adjust canvas size to window
function adjustCanvasSize() {
if (window.innerHeight >= window.innerWidth * (3/4)) {
var s = window.innerWidth;
var t = Math.floor(s * (3/4));
Module.setCanvasSize(s * resModifier, t * resModifier);
canvasMask.style.width = s + "px";
canvasMask.style.height = t + "px";
} else {
var s = window.innerHeight;
var t = Math.floor(s * (4/3));
Module.setCanvasSize(t * resModifier, s * resModifier);
canvasMask.style.width = t + "px";
canvasMask.style.height = s + "px";
}
}
// logging
function log(log, userInput) {
console.log(log);
wconsole.textContent += (userInput ? "> " + userInput + "\n\t" + JSON.stringify(log) : log) + "\n";
wconsole.scrollTo({top: wconsole.scrollHeight});
if (typeof log == "string") {
// export state
if (log.includes("New state file is ready to be read")) saveStateFunc();
// await log queue
if (Object.keys(awaitLogQueue).length) {
var lq = Object.keys(awaitLogQueue);
for (var i = 0; i < lq.length; i++) {
if (log.toLowerCase().includes(lq[i].toLowerCase())) {
awaitLogQueue[lq[i]](log);
delete awaitLogQueue[lq[i]];
}
}
}
}
}
function awaitLog(contains, callback, timeout, expire) {
awaitLogQueue[contains] = callback;
if (timeout && !isNaN(timeout)) {
window.setTimeout(function() {
if (awaitLogQueue[contains]) {
expire();
delete awaitLogQueue[contains];
}
}, timeout);
}
}
// xhr
function grab(url, type, success, fail) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.overrideMimeType("text/plain; charset=x-user-defined");
req.responseType = type;
req.onload = function() {
if (req.status >= "400") {
if (fail) fail(req.status);
} else {
if (success) success(this.response);
}
}
req.send();
}
// file readers
function readFile(file, callback) {
var reader = new FileReader();
reader.onload = function() {
callback(this.result);
}
reader.readAsArrayBuffer(file);
}
function downloadFile(data, name, mime) {
var a = document.createElement("a");
a.download = name;
a.href = URL.createObjectURL(new Blob([data], {type: mime || "application/octet-stream"}));
a.click();
window.setTimeout(function() {
URL.revokeObjectURL(a.href);
}, 2000);
}
function uploadFile(accept, callback) {
var input = document.createElement("input");
input.type = "file";
input.accept = accept;
input.onchange = function() {
let file = this.files[0];
readFile(file, function(data) {
callback({name: file.name, data: data});
});
}
input.click();
}
// scripts
function getScript(url, callback, err) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
script.onload = function() {
if (callback) callback();
}
script.onerror = function(e) {
document.body.removeChild(script);
if (err) err(e);
}
document.body.appendChild(script);
}
function getCore(name, callback, err) {
getScript("./" + name + "_libretro.js", callback, err);
}
// check for updates
function checkForUpdates() {
grab("https://cdn.jsdelivr.net/gh/BinBashBanana/webretro@latest/assets/info.json", "text", function(text) {
try {
var updateObj = JSON.parse(text);
if (updateObj.webretro) {
latestVersion = updateObj.webretro;
if (updateObj.versions[webretroVersion.toString()]) versionIndicator.title = "New features in this version:\n\n- " + updateObj.versions[webretroVersion.toString()].changeList.join("\n- ");
if (latestVersion > webretroVersion && updateObj.versions[latestVersion.toString()]) {
updateNotice.textContent = "New webretro version available: v" + latestVersion.toString() + ". Features:\n\n- " + updateObj.versions[latestVersion.toString()].changeList.join("\n- ") + "\n\nThe site owner(s) can apply the update.";
updateNotice.style.display = "initial";
}
}
} catch (e) {
log(e);
}
});
}
// unzip file
function unzipFile(data, exts, callback, empty, notfound) {
new zip.ZipReader(new zip.Uint8ArrayReader(data)).getEntries().then(function(entries) {
if (entries.length) {
for (var i = 0; i < entries.length; i++) {
if (exts.split(", ").includes("." + u8atoutf8(entries[i].rawFilename).split(".").slice(-1)[0])) {
let name = u8atoutf8(entries[i].rawFilename);
entries[i].getData(new zip.Uint8ArrayWriter()).then(function(uzd) {
callback(name, uzd);
});
break;
}
if (i == entries.length - 1 && notfound) notfound();
}
} else if (empty) empty();
});
}
// zip files
async function zipFiles(files, callback, replaceName) {
var u8aWriter = new zip.Uint8ArrayWriter("application/zip");
var writer = new zip.ZipWriter(u8aWriter);
for (var i = 0; i < files.length; i++) {
await writer.add(replaceName ? files[i].name.replace("rom", romName) : files[i].name, new zip.Uint8ArrayReader(files[i].data));
}
await writer.close();
var zipped = await u8aWriter.getData();
callback(zipped);
}
// uauth uploads
function handleWebFile(data) {
if (data.message == "success") {
ffd.style.display = "none";
romUploadCallback(data.name, data.data);
} else if (data.message == "error") {
alert("There was an error with the file picker. This may mean that you have to allow popup windows.");
}
}
function uploadWebFile(type, exts) {
uauth.open(type, exts.split(", "), handleWebFile);
}
// rom upload
function readyRomUploads(exts) {
romUploadsReady = true;
// when a rom file is chosen
upload.onclick = function() {
uploadFile(exts, function(file) {
ffd.style.display = "none";
log('Succesfully read ROM file "' + file.name + '"');
romUploadCallback(file.name, file.data);
});
}
// web uploads
googleDriveUpload.onclick = function() {
uploadWebFile("drive", exts);
}
dropboxUpload.onclick = function() {
uploadWebFile("dropbox", exts);
}
oneDriveUpload.onclick = function() {
uploadWebFile("onedrive", exts);
}
// file drop (we need these to be global so they can be removed later)
window.fileDragEnter = function(e) {
if (e.dataTransfer.types.includes("Files")) ffd.classList.add("filehover");
}
window.fileDragOver = function(e) {
e.preventDefault();
}
window.fileDropped = function(e) {
if (e.dataTransfer.types.includes("Files")) {
e.preventDefault();
ffd.style.display = "none";
let file = event.dataTransfer.files[0];
readFile(file, function(data) {
log('Succesfully read ROM file "' + file.name + '"');
romUploadCallback(file.name, data);
});
}
}
document.addEventListener("dragenter", fileDragEnter, false);
document.addEventListener("dragover", fileDragOver, false);
document.addEventListener("drop", fileDropped, false);
}
// rom fetch
function readyRomFetch() {
var romloc = (/^(http:\/\/|https:\/\/|\/\/)/i).test(queries.rom) ? queries.rom : "roms/" + queries.rom;
var romFilename = queries.rom.split("/").slice(-1)[0];
grab(romloc, "arraybuffer", function(data) {
log("Succesfully fetched ROM from " + romloc);
romMode = "querystring";
romUploadCallback(romFilename, data);
}, function(error) {
alert("Could not get ROM at " + romloc + " (Error " + error + ")");
romMode = "upload";
ffd.style.display = "block";
});
}
// console window
var conw = new jswindow({title: "Console", icon: "assets/terminal.svg"});
var wconsole = document.createElement("textarea");
wconsole.classList.add("console");
wconsole.setAttribute("spellcheck", "false");
wconsole.setAttribute("readonly", "");
wconsole.wconsolemarker = document.createElement("span");
wconsole.wconsolemarker.classList.add("consolemarker");
wconsole.wconsoleinput = document.createElement("input");
wconsole.wconsoleinput.type = "text";
wconsole.wconsoleinput.classList.add("consoleinput");
wconsole.wconsoleinput.title = "You can type things here as though you were using the browser console.";
wconsole.wconsoleinput.setAttribute("spellcheck", "false");
wconsole.wconsolemarker.onclick = function() { wconsole.wconsoleinput.focus(); }
wconsole.wconsoleinput.onkeydown = function(e) {
e.stopPropagation();
if (e.keyCode == 13) {
log(eval(this.value), this.value);
this.value = "";
}
}
conw.innerWindow.appendChild(wconsole);
conw.innerWindow.appendChild(wconsole.wconsolemarker);
conw.innerWindow.appendChild(wconsole.wconsoleinput);
consoleButton.onclick = function() {
conw.open({width: 450, height: 250, left: 100, top: 50});
wconsole.wconsoleinput.focus();
wconsole.scrollTo({top: wconsole.scrollHeight});
}
if (queries.hasOwnProperty("console")) conw.open({width: 450, height: 250, left: 100, top: 50});
// modal windows (managers)
function openManager(type) {
if (managers[type]) {
if (managerClosed[currentManager]) managerClosed[currentManager]();
currentManager = type;
if (managerOpened[type]) managerOpened[type]();
managerTitle.textContent = managerNames[type] || type + "s";
clearManagers();
managers[type].style.display = "block";
modals.style.display = "block";
}
}
function clearManagers() {
Object.values(managers).forEach(function(e) {
e.style.display = "none";
});
}
managerClose.onclick = function() {
modals.style.display = "none";
clearManagers();
managerTitle.textContent = "";
if (managerClosed[currentManager]) managerClosed[currentManager]();
currentManager = undefined;
}
// --- code for the keybind manager ---
// convert between config strings and objects
function configStrToObj(str) {
var convert1 = str.slice(0, -1).split("\n");
var convert2 = {};
for (var i = 0; i < convert1.length; i++) {
var convert3 = convert1[i].split(" = ");
convert2[convert3[0]] = convert3[1].slice(1, -1);
}
return convert2;
}
function configObjToStr(obj) {
var convert1 = Object.keys(obj);
var convert2 = "";
for (var i = 0; i < convert1.length; i++) {
convert2 += convert1[i] + ' = "' + obj[convert1[i]] + '"\n';
}
return convert2;
}
// load config saved in localStorage
var defaultKeybindsObj = configStrToObj(defaultKeybinds);
var savedKeybindsObj = window.localStorage.getItem("RetroArch_settings_keybinds") ? configStrToObj(window.localStorage.getItem("RetroArch_settings_keybinds")) : Object.assign({}, defaultKeybindsObj);
var keybindsObj = Object.assign({}, savedKeybindsObj);
var validKeybinds = Object.keys(defaultKeybindsObj);
// update the config list
function createConfigList() {
keybindTable.innerHTML = "";
// make the list
for (var i = 0; i < validKeybinds.length; i++) {
keybindTable.innerHTML += "
" + validKeybinds[i] + "
" + keybindsObj[validKeybinds[i]] + "
";
}
// highlight conflicting keys
var keysList = Object.values(keybindsObj);
for (var i = 0; i < validKeybinds.length; i++) {
var matches = keysList.filter(v => v == keybindsObj[validKeybinds[i]]);
if (matches.length > 1 && !(matches[0] == "nul")) keybindTable.children[i].lastElementChild.classList.add("conflict");
}
}
// rebinding a key
keybindTable.onclick = function(e) {
if (e.target.tagName == "TD" && !e.target.nextElementSibling) {
let valueElement = e.target;
let keyNo = Array.from(keybindTable.children).indexOf(e.target.parentElement);
valueElement.classList.remove("conflict");
valueElement.textContent = "press a key (escape to unbind)";
function newKeyHandler(e) {
if (e.code == "Escape") {
keybindsObj[validKeybinds[keyNo]] = "nul";
createConfigList();
} else {
keybindsObj[validKeybinds[keyNo]] = codeToConfigIDMap[e.code] || "nul";
createConfigList();
}
finishKeybindInput();
}
function cancelKeybindInput() {
finishKeybindInput();
createConfigList();
}
function finishKeybindInput() {
document.removeEventListener("keydown", newKeyHandler);
document.removeEventListener("mousedown", cancelKeybindInput);
}
document.addEventListener("keydown", newKeyHandler, false);
document.addEventListener("mousedown", cancelKeybindInput, false);
}
}
function tryApplyConfig() {
if (emulatorStarted) {
FS.writeFile("/home/web_user/retroarch/userdata/retroarch.cfg", nulKeys + configObjToStr(savedKeybindsObj) + extraConfig);
Module._cmd_reload_config();
}
}
// save the keybinds to localStorage, and apply them
saveKeybinds.onclick = function() {
savedKeybindsObj = Object.assign({}, keybindsObj);
window.localStorage.setItem("RetroArch_settings_keybinds", configObjToStr(savedKeybindsObj));
tryApplyConfig();
alert("Saved!");
}
resetKeybinds.onclick = function() {
if (confirm("Are you sure you want to reset all of the keybinds to their default values?")) {
savedKeybindsObj = Object.assign({}, defaultKeybindsObj);
keybindsObj = Object.assign({}, savedKeybindsObj);
window.localStorage.removeItem("RetroArch_settings_keybinds");
createConfigList();
tryApplyConfig();
}
}
// --- code for the screenshot manager ---
// zip and download all of the screenshots in the list
downloadAllScreenshots.onclick = function() {
if (screenshotDatas.length) {
zipFiles(screenshotDatas, function(zd) {
downloadFile(zd, "screenshots-" + getTime() + ".zip", "application/zip");
}, true);
} else {
alert("There are no screenshots to download!");
}
}
// update the screenshot list
function createScreenshotList() {
var screenshots = FS.analyzePath("/home/web_user/retroarch/userdata/screenshots/").exists ? FS.readdir("/home/web_user/retroarch/userdata/screenshots/").filter(k => ![".", ".."].includes(k)) : [];
screenshotsDiv.innerHTML = "";
for (var i = 0; i < screenshots.length; i++) {
var screenshotData = FS.readFile("/home/web_user/retroarch/userdata/screenshots/" + screenshots[i]);
var blobUrl = window.URL.createObjectURL(new Blob([screenshotData], {type: "image/png"}));
screenshotDatas[i] = {name: screenshots[i], data: screenshotData};
screenshotObjUrls[i] = blobUrl;
screenshotsDiv.innerHTML += '
' + "
";
}
}
// why I didn't just use the DOM? I don't know
screenshotsDiv.onclick = function(e) {
if (e.target.tagName == "INPUT") {
var screenshotNo = Array.from(screenshotsDiv.children).indexOf(e.target.parentElement);
switch(e.target.dataset.action) {
case "download":
downloadFile(screenshotDatas[screenshotNo].data, screenshotDatas[screenshotNo].name.replace("rom", romName), "image/png");
break;
case "delete":
if (confirm("Are you sure you want to delete this screenshot?")) {
// doing all this is probably more efficient then reloading all of the screenshots
window.URL.revokeObjectURL(screenshotObjUrls[screenshotNo]);
FS.unlink("/home/web_user/retroarch/userdata/screenshots/" + screenshotDatas[screenshotNo].name);
screenshotObjUrls.splice(screenshotNo, 1);
screenshotDatas.splice(screenshotNo, 1);
e.target.parentElement.remove();
}
break;
}
}
}
// --- code for the save/state manager ---
function updateQuotaDisplay() {
navigator.storage.estimate().then(function(info) {
quotaText.textContent = "Storage used (estimate): " + bytesToHumanReadable(info.usage) + " / " + bytesToHumanReadable(info.quota) + " (" + (info.usage / info.quota).toFixed(2) + "%)";
});
}
// update the save list
function createSaveList() {
updateQuotaDisplay();
getAllIdbItems().then(function(items) {
saveTable.innerHTML = "";
// make the list
for (var i = 0; i < items.length; i++) {
if ((/^RetroArch_(saves|states)_/).test(items[i].key)) {
var sName = items[i].key.replace(/^RetroArch_(saves|states)_/, "");
var sType = (/^RetroArch_saves_/).test(items[i].key) ? "save" : "state";
saveIDs.push({id: items[i].key, name: sName, type: sType});
saveTable.innerHTML += "
" + capitalize(sType) + ": " + sName + '
DownloadDelete
';
}
}
});
}
saveTable.onclick = function(e) {
if (e.target.tagName == "SPAN") {
let saveNo = Array.from(saveTable.children).indexOf(e.target.parentElement.parentElement);
switch(e.target.dataset.action) {
case "download":
getIdbItem(saveIDs[saveNo].id).then(function(data) {
downloadFile(data, "game-" + saveIDs[saveNo].type + "-" + saveIDs[saveNo].name + "-" + getTime() + (saveIDs[saveNo].type == "save" ? ".srm" : ".state"));
});
break;
case "delete":
if (confirm("Are you sure you want to delete this " + saveIDs[saveNo].type + ' for "' + saveIDs[saveNo].name + '"?') && confirm("Really really sure?")) {
removeIdbItem(saveIDs[saveNo].id);
saveIDs.splice(saveNo, 1);
e.target.parentElement.parentElement.remove();
updateQuotaDisplay();
}
break;
}
}
}
// --- end manager-specific code ---
var managerOpened = {
"keybind": function() {
createConfigList();
},
"screenshot": function() {
createScreenshotList();
},
"save": function() {
createSaveList();
}
};
var managerClosed = {
"keybind": function() {
keybindsObj = Object.assign({}, savedKeybindsObj);
},
"screenshot": function() {
// clear the blob: urls used for the screenshots
for (var i = 0; i < screenshotObjUrls.length; i++) {
window.URL.revokeObjectURL(screenshotObjUrls[i]);
}
screenshotObjUrls = [];
screenshotDatas = [];
},
"save": function() {
saveIDs = [];
}
};
// opening the managers
keybindsButton.onclick = function(e) {
e.preventDefault();
openManager("keybind");
}
screenshotsButton.onclick = function() {
openManager("screenshot");
}
savesButton.onclick = function(e) {
e.preventDefault();
openManager("save");
}
statesButton.onclick = function(e) {
e.preventDefault();
openManager("save");
};
// ---------- START LOAD ----------
(function() {
checkForUpdates();
// ?system query
if (!queries.core && queries.system) {
var detectedCore = Object.keys(systems).find(k => systems[k].toLowerCase() == queries.system.toLowerCase());
if (installedCores.includes(detectedCore)) {
queries.core = detectedCore;
} else if (queries.system.toLowerCase() == "autodetect") {
queries.core = "autodetect";
} else {
alert("Invalid core (" + detectedCore + ")");
}
}
// ?core query
if (queries.core) {
if (!window.navigator.userAgent.toLowerCase().includes("chrom")) alert("Best performance on Chrome!");
// show hover menu
hoverMenu.style.display = "block";
versionIndicator.textContent = "v" + webretroVersion.toString();
if (queries.core.toLowerCase() == "autodetect") {
romUploadCallback = autodetectCoreHandler;
systemName.textContent = "";
readyRomUploads(".zip, " + allFileExts);
} else {
romUploadCallback = initFromFile;
core = queries.core;
setStatus("Getting core");
if (core == "desmume") sramExt = ".dsv";
// detect system for ROM upload
systemName.textContent = systems[core] || "";
getCore(core, function() {
removeStatus("Getting core");
log("Got core: " + core);
if (romMode != "querystring") document.title = core + " | webretro";
readyRomUploads(".zip, .bin, " + fileExts[systems[core]]);
}, function() {
// core loading error
alert('Could not load specified core "' + core + '". Here is a list of available cores.');
ffdContent.innerHTML = "
" + aCoreList + "
";
ffd.style.display = "block";
});
}
// ?rom query
if (queries.rom) {
readyRomFetch();
} else {
// prompt user to upload ROM file
romMode = "upload";
ffd.style.display = "block";
}
} else {
// no core specified
ffdContent.innerHTML = "
" + aCoreList + "
";
ffd.style.display = "block";
}
})();
// ----------- END LOAD -----------
// start emulator from file name and data
function initFromFile(name, data) {
var dataView = new Uint8Array(data);
if (name.split(".").slice(-1)[0] == "zip") {
log("Zip file detected, unzipping...");
unzipFile(dataView, fileExts[systems[core]], function(name, contents) {
romName = name.split(".")[0];
readyForInit(contents);
}, function() {
alert("That zip file appears to be empty!");
}, function() {
alert("Couldn't find a valid ROM file in that zip file. Are you using the right core? This is " + systems[core] + ". (The ROM has to be at the base directory of the zip file)");
});
} else {
romName = name.split(".")[0];
readyForInit(dataView);
}
}
// autodetect core mode
function autodetectCoreHandler(name, data) {
var dataView = new Uint8Array(data);
if (name.split(".").slice(-1)[0] == "zip") {
log("Zip file detected, unzipping...");
unzipFile(dataView, allFileExts, function(name, contents) {
romName = name.split(".")[0];
autodetectCore(name, contents);
}, function() {
alert("That zip file appears to be empty!");
}, function() {
alert("Couldn't find a valid ROM file in that zip file. (The ROM has to be at the base directory of the zip file)");
});
} else {
romName = name.split(".")[0];
autodetectCore(name, dataView);
}
}
function autodetectCore(name, data) {
var nameExt = "." + name.split(".").slice(-1)[0];
var detectedCore;
var fileExtsArray = Object.keys(fileExts);
for (var i = 0; i < fileExtsArray.length; i++) {
if (fileExts[fileExtsArray[i]].split(", ").includes(nameExt)) {
detectedCore = Object.keys(systems).find(k => systems[k] == fileExtsArray[i]);
break;
}
}
var detectedSystem = systems[detectedCore] || "unknown";
detectedCore = detectedCore || "unknown";
if (allValidFileExts.split(", ").includes(nameExt)) {
core = detectedCore;
setStatus("Getting core");
if (core == "desmume") sramExt = ".dsv";
getCore(core, function() {
removeStatus("Getting core");
log("Got core: " + core);
readyForInit(data);
});
} else {
alert("That is a " + detectedSystem + " file! " + detectedCore + " (" + detectedSystem + ") is not currently supported.");
}
}
// if the ROM is specified in the querystring, we will need to wait until the user has clicked to start the emulator
function readyForInit(data) {
document.title = romName + " | webretro";
if (queries.romshift) data = avShift(data, parseInt(queries.romshift));
// remove the file drop listeners
if (romUploadsReady) {
document.removeEventListener("dragenter", fileDragEnter);
document.removeEventListener("dragover", fileDragOver);
document.removeEventListener("drop", fileDropped);
}
if (romMode == "querystring") {
// start button (don't delete this section, audio contexts are not allowed to start until a user gesture on the page, in this case, clicking the start button)
startButton.style.display = "initial";
startButton.onclick = function() {
startButton.style.display = "none";
initFromData(data);
}
} else {
initFromData(data);
}
}
// prepare FS with bundle
function prepareBundle() {
setStatus("Getting assets");
log("Starting bundle fetch");
let bundleSTime = performance.now();
grab(bundleCdnLatest + "bundle/indexedfiles-v2.txt", "text", function(data) {
var splitData = data.split(",,,\n");
fsBundleDirs = JSON.parse(splitData[0]);
fsBundleFiles = splitData[1].split("\n");
// make the paths
FS.createPath("/", "home/web_user/retroarch/bundle", true, true);
for (var i = 0; i < fsBundleDirs.length; i++) {
FS.createPath(baseFsBundleDir + fsBundleDirs[i][0], fsBundleDirs[i][1], true, true);
}
// make the files
for (let i = 0; i < fsBundleFiles.length; i++) {
grab(bundleCdn + "bundle" + fsBundleFiles[i], "arraybuffer", function(data) {
FS.writeFile(baseFsBundleDir + fsBundleFiles[i], new Uint8Array(data));
if (i == fsBundleFiles.length - 1) donePreparingBundle(performance.now() - bundleSTime);
}, function() {
bundleErrors += 1;
if (i == fsBundleFiles.length - 1) donePreparingBundle(performance.now() - bundleSTime);
});
}
}, function() {
log("Failed to get asset bundle, skipping");
bundleReady = true;
removeStatus("Getting assets");
});
}
function donePreparingBundle(tooktime) {
bundleReady = true;
removeStatus("Getting assets");
log("Finished bundle fetch in " + (tooktime / 1000).toFixed(1) + " seconds, " + bundleErrors + " errors");
}
// tell the user to not rename the rom
function doNotRename() {
if (romMode == "upload" && !window.localStorage.getItem("webretro_settings_pastFirstSave")) {
alert("WARNING: Do not rename your ROM file after this! The save data is specific to the ROM name!");
window.localStorage.setItem("webretro_settings_pastFirstSave", "true");
}
}
// save game
function saveSRAM() {
Module._cmd_savefiles();
window.setTimeout(function() {
if (FS.analyzePath("/home/web_user/retroarch/userdata/saves/rom" + sramExt).exists) {
setIdbItem("RetroArch_saves_" + romName, FS.readFile("/home/web_user/retroarch/userdata/saves/rom" + sramExt));
new sideAlert("Saved", 3000);
readySaveReaders();
doNotRename();
} else {
autosave.checked = false;
new sideAlert("This game does not save!", 3000);
}
}, 1000);
}
// save state
function saveStateFunc() {
window.setTimeout(function() {
if (FS.analyzePath("/home/web_user/retroarch/userdata/states/rom.state").exists) {
setIdbItem("RetroArch_states_" + romName, FS.readFile("/home/web_user/retroarch/userdata/states/rom.state"));
doNotRename();
} else {
new sideAlert("There was an error saving state. Please try again.", 5000);
}
}, 100);
}
// autosaving
function autosaveSRAM() {
if (autosave.checked && !document.hidden && !isPaused) {
new sideAlert("Autosaving...", 3000);
saveSRAM();
}
window.setTimeout(function() {
autosaveSRAM();
}, 300000);
}
// more functions for state buttons
function readyStateReaders() {
if (!stateReadersReady) {
stateReadersReady = true;
loadState.classList.remove("disabled");
exportState.classList.remove("disabled");
undoSaveState.classList.remove("disabled");
loadState.onclick = function() {
Module._cmd_load_state();
readyStateReaders2();
}
exportState.onclick = function() {
downloadFile(FS.readFile("/home/web_user/retroarch/userdata/states/rom.state"), "game-state-" + romName + "-" + getTime() + ".state");
}
undoSaveState.onclick = function() {
Module._cmd_undo_save_state();
}
// also allow statereaders2 on load state press
document.addEventListener("keydown", function(e) {
if (!stateReaders2Ready && (e.code == Object.keys(codeToConfigIDMap).find(k => codeToConfigIDMap[k] == savedKeybindsObj.input_load_state))) readyStateReaders2();
}, false);
}
}
// even more functions for state buttons
function readyStateReaders2() {
if (!stateReaders2Ready) {
stateReaders2Ready = true;
undoLoadState.classList.remove("disabled");
undoLoadState.onclick = function() {
Module._cmd_undo_load_state();
}
}
}
// more functions for save buttons
function readySaveReaders() {
if (!saveReadersReady) {
saveReadersReady = true;
exportSave.classList.remove("disabled");
exportSave.onclick = function() {
downloadFile(FS.readFile("/home/web_user/retroarch/userdata/saves/rom" + sramExt), "game-sram-" + romName + "-" + getTime() + sramExt);
}
}
}
// runs after emulator starts
function afterStart() {
emulatorStarted = true;
// remove loading text
canvas.style.background = "none";
adjustCanvasSize();
// functions for save and state buttons
saveState.classList.remove("disabled");
importState.classList.remove("disabled");
saveGame.classList.remove("disabled");
importSave.classList.remove("disabled");
autosave.removeAttribute("disabled");
autosave.parentElement.classList.remove("disabled");
saveState.onclick = function() {
Module._cmd_save_state();
readyStateReaders();
}
importState.onclick = function() {
uploadFile(".bin, .state, .save, .dat, .gam, .sav, application/*", function(file) {
setIdbItem("RetroArch_states_" + romName, new Uint8Array(file.data));
FS.writeFile("/home/web_user/retroarch/userdata/states/rom.state", new Uint8Array(file.data));
new sideAlert("Imported state (Now press load state)", 3000);
readyStateReaders();
});
}
saveGame.onclick = function() {
new sideAlert("Saving...", 3000);
saveSRAM();
}
importSave.onclick = function() {
uploadFile(".bin, .srm, .sram, .ram, .gam, .sav, .dsv, application/*", function(file) {
autosave.checked = false;
setIdbItem("RetroArch_saves_" + romName, new Uint8Array(file.data));
if (confirm("Save imported. Reloading now for changes to take effect.")) {
window.onbeforeunload = function() {}
window.location.reload();
}
});
}
// also allow state readers on save state press
document.addEventListener("keydown", function(e) {
if (!stateReadersReady && (e.code == Object.keys(codeToConfigIDMap).find(k => codeToConfigIDMap[k] == savedKeybindsObj.input_save_state))) readyStateReaders();
}, false);
// start autosave loop
window.setTimeout(function() {
autosaveSRAM();
}, 300000);
// toggle between sharp and smooth canvas graphics
smooth.removeAttribute("disabled");
smooth.parentElement.classList.remove("disabled");
smooth.onclick = function() {
if (this.checked) {
canvas.className = "textureSmooth";
} else {
canvas.className = "texturePixelated";
}
}
// higher resolution
doubleRes.removeAttribute("disabled");
doubleRes.parentElement.classList.remove("disabled");
doubleRes.onclick = function() {
if (this.checked) {
resModifier = 2;
adjustCanvasSize();
} else {
resModifier = 1;
adjustCanvasSize();
}
}
// pause and resume
pause.classList.remove("disabled");
pause.onclick = function() {
if (this.textContent.trim() == "Pause") {
Module.pauseMainLoop();
isPaused = true;
this.textContent = "Resume";
document.body.classList.add("paused");
} else {
Module.resumeMainLoop();
isPaused = false;
this.textContent = "Pause";
document.body.classList.remove("paused");
}
}
resumeOverlay.onclick = function() {
pause.click();
}
// toggle menu
menuButton.classList.remove("disabled");
menuButton.onclick = function() {
Module._cmd_toggle_menu();
}
// screenshot button
takeScreenshot.classList.remove("disabled");
takeScreenshot.onclick = function() {
Module._cmd_take_screenshot();
}
// flash the menu on first use
if (!window.localStorage.getItem("webretro_settings_pastFirstStart")) {
hoverMenu.classList.add("show");
hoverMenuIndicator.classList.add("show");
window.setTimeout(function() {
hoverMenu.classList.remove("show");
hoverMenuIndicator.classList.remove("show");
}, 3000);
window.localStorage.setItem("webretro_settings_pastFirstStart", "true");
}
// ctrl+v inside canvas
document.addEventListener("keydown", function(e) {
if (e.ctrlKey && e.key == "v") {
fakeKeyPress({code: "Backspace"});
navigator.clipboard.readText().then(function(text) {
sendText(text);
});
}
}, false);
}
// start
function initFromData(data) {
window.onbeforeunload = function() { return true; }
async function waitForReady() {
if (wasmReady && bundleReady) {
setStatus("Waiting for emulator");
log("Initializing with " + data.byteLength + " bytes of data");
updateNotice.style.display = "none";
canvas.addEventListener("contextmenu", function(e) {
e.preventDefault();
}, false);
window.addEventListener("resize", adjustCanvasSize, false);
adjustCanvasSize();
// prevent defaults for key presses
document.addEventListener("keydown", function(e) {
if (pdKeys.includes(e.which)) e.preventDefault();
}, false);
// move the saves and states from the old version (localStorage) to indexedDB
await tryLsToIdb();
// rom
FS.writeFile("/rom.bin", data);
// SMAS brick fix
if (systems[core] == "SNES") {
var hash = md5(u8atoutf8(data));
if (smasBrickFix.hasOwnProperty(hash)) {
FS.writeFile("/rom.ips", new Uint8Array(smasBrickFix[hash]));
new sideAlert("SMAS Bricks Fixed!", 5000);
}
}
// load save
var cSave = await getIdbItem("RetroArch_saves_" + romName);
if (cSave) {
FS.createPath("/", "home/web_user/retroarch/userdata/saves", true, true);
FS.writeFile("/home/web_user/retroarch/userdata/saves/rom" + sramExt, cSave);
new sideAlert("Save loaded for " + romName, 5000);
log("Save loaded for " + romName);
readySaveReaders();
}
// import state
var cState = await getIdbItem("RetroArch_states_" + romName);
if (cState) {
FS.createPath("/", "home/web_user/retroarch/userdata/states", true, true);
FS.writeFile("/home/web_user/retroarch/userdata/states/rom.state", cState);
new sideAlert("State imported for " + romName, 5000);
log("State imported for " + romName);
readyStateReaders();
}
// config
FS.createPath("/", "home/web_user/retroarch/userdata", true, true);
FS.writeFile("/home/web_user/retroarch/userdata/retroarch.cfg", nulKeys + configObjToStr(savedKeybindsObj) + extraConfig);
// start
Module.callMain(Module.arguments);
adjustCanvasSize();
window.setTimeout(afterStart, 2000);
} else {
window.setTimeout(waitForReady, 1000);
}
}
waitForReady();
}
var Module = {
canvas: canvas,
noInitialRun: true,
arguments: ["/rom.bin", "--verbose"],
onRuntimeInitialized: function() {
wasmReady = true;
log("WASM ready");
// fetch asset bundle
if (queries.hasOwnProperty("nobundle")) {
bundleReady = true;
log("Skipping bundle");
} else {
prepareBundle();
}
},
print: function(text) {
log("stdout: " + text);
},
printErr: function(text) {
log("stderr: " + text);
}
};