Compare commits

...

4 commits

Author SHA1 Message Date
735c20b3be Append -beta to the version number, define compression on window
Some checks failed
Make Latest Folder On EmulatorJS CDN / run (push) Has been cancelled
2024-08-03 22:12:43 -05:00
e8c9d0d28b Move compression related functions to another file 2024-08-03 22:00:00 -05:00
41c7850dfa Modularize EmulatorJS 2024-08-03 20:54:05 -05:00
10e5320261 Add Emulatorjs exit button 2024-08-03 20:19:48 -05:00
6 changed files with 236 additions and 173 deletions

View file

@ -6,7 +6,8 @@
"storage.js",
"gamepad.js",
"GameManager.js",
"socket.io.min.js"
"socket.io.min.js",
"compression.js"
];

View file

@ -9,7 +9,8 @@ const scripts = [
"storage.js",
"gamepad.js",
"GameManager.js",
"socket.io.min.js"
"socket.io.min.js",
"compression.js"
];
let code = "(function() {\n";
for (let i=0; i<scripts.length; i++) {

View file

@ -39,14 +39,17 @@ class EJS_GameManager {
this.writeFile("/home/web_user/retroarch/userdata/retroarch.cfg", this.getRetroArchCfg());
this.FS.mount(IDBFS, {}, '/data/saves');
this.FS.syncfs(true, () => {});
this.FS.mount(this.FS.filesystems.IDBFS, {autoPersist: true}, '/data/saves');
//this.FS.syncfs(true, () => {});
this.initShaders();
this.EJS.addEventListener(window, "beforeunload", () => {
this.saveSaveFiles();
this.FS.syncfs(() => {});
this.EJS.on("exit", () => {
this.toggleMainLoop(0);
this.functions.saveSaveFiles();
setTimeout(() => {
try {window.abort()} catch(e){};
}, 1000);
})
}
loadExternalFiles() {
@ -159,7 +162,7 @@ class EJS_GameManager {
return new Promise(async resolve => {
while (1) {
try {
FS.stat("/screenshot.png");
this.FS.stat("/screenshot.png");
return resolve(this.FS.readFile("/screenshot.png"));
} catch(e) {}
@ -268,14 +271,14 @@ class EJS_GameManager {
}
for (let i=0; i<fileNames.length; i++) {
const contents = " FILE \""+fileNames[i]+"\" BINARY\n TRACK 01 MODE1/2352\n INDEX 01 00:00:00";
FS.writeFile("/"+baseFileName+"-"+i+".cue", contents);
this.FS.writeFile("/"+baseFileName+"-"+i+".cue", contents);
}
if (fileNames.length > 1) {
let contents = "";
for (let i=0; i<fileNames.length; i++) {
contents += "/"+baseFileName+"-"+i+".cue\n";
}
FS.writeFile("/"+baseFileName+".m3u", contents);
this.FS.writeFile("/"+baseFileName+".m3u", contents);
}
return (fileNames.length === 1) ? baseFileName+"-0.cue" : baseFileName+".m3u";
}
@ -299,7 +302,7 @@ class EJS_GameManager {
if (paths[i] === "") continue;
cp += "/"+paths[i];
if (!FS.analyzePath(cp).exists) {
FS.mkdir(cp);
this.FS.mkdir(cp);
}
}
this.FS.writeFile(path, data);
@ -344,15 +347,15 @@ class EJS_GameManager {
}
saveSaveFiles() {
this.functions.saveSaveFiles();
this.FS.syncfs(false, () => {});
//this.FS.syncfs(false, () => {});
}
supportsStates() {
return !!this.functions.supportsStates();
}
getSaveFile() {
this.saveSaveFiles();
const exists = FS.analyzePath(this.getSaveFilePath()).exists;
return (exists ? FS.readFile(this.getSaveFilePath()) : null);
const exists = this.FS.analyzePath(this.getSaveFilePath()).exists;
return (exists ? this.FS.readFile(this.getSaveFilePath()) : null);
}
loadSaveFiles() {
this.clearEJSResetTimer();

104
data/src/compression.js Normal file
View file

@ -0,0 +1,104 @@
class EJS_COMPRESSION {
cache = {};
constructor(EJS) {
this.EJS = EJS;
}
isCompressed(data) { //https://www.garykessler.net/library/file_sigs.html
//todo. Use hex instead of numbers
if ((data[0] === 80 && data[1] === 75) && ((data[2] === 3 && data[3] === 4) || (data[2] === 5 && data[3] === 6) || (data[2] === 7 && data[3] === 8))) {
return 'zip';
} else if (data[0] === 55 && data[1] === 122 && data[2] === 188 && data[3] === 175 && data[4] === 39 && data[5] === 28) {
return '7z';
} else if ((data[0] === 82 && data[1] === 97 && data[2] === 114 && data[3] === 33 && data[4] === 26 && data[5] === 7) && ((data[6] === 0) || (data[6] === 1 && data[7] == 0))) {
return 'rar';
}
return null;
}
decompress(data, updateMsg, fileCbFunc) {
const compressed = this.isCompressed(data.slice(0, 10));
if (compressed === null) {
if (typeof fileCbFunc === "function") {
fileCbFunc("!!notCompressedData", data);
}
return new Promise(resolve => resolve({"!!notCompressedData": data}));
}
return this.decompressFile(compressed, data, updateMsg, fileCbFunc);
}
getWorkerFile(method) {
return new Promise((resolve, reject) => {
let path, obj;
if (method === "7z") {
path = "compression/extract7z.js";
obj = "sevenZip";
} else if (method === "zip") {
path = "compression/extractzip.js";
obj = "zip";
} else if (method === "rar") {
path = "compression/libunrar.js";
obj = "rar";
}
if (this.cache[obj]) {
return this.cache[obj];
}
this.EJS.downloadFile(path, (res) => {
if (res === -1) {
this.EJS.startGameError(this.EJS.localization('Network Error'));
return;
}
if (method === "rar") {
// Do not cache rar. This shouldnt normally be used more than once anyways.
this.EJS.downloadFile("compression/libunrar.wasm", (res2) => {
if (res2 === -1) {
this.EJS.startGameError(this.EJS.localization('Network Error'));
return;
}
const path = URL.createObjectURL(new Blob([res2.data], {type: "application/wasm"}));
let data = '\nlet dataToPass = [];\nModule = {\n monitorRunDependencies: function(left) {\n if (left == 0) {\n setTimeout(function() {\n unrar(dataToPass, null);\n }, 100);\n }\n },\n onRuntimeInitialized: function() {\n },\n locateFile: function(file) {\n return \''+path+'\';\n }\n};\n'+res.data+'\nlet unrar = function(data, password) {\n let cb = function(fileName, fileSize, progress) {\n postMessage({"t":4,"current":progress,"total":fileSize, "name": fileName});\n };\n\n let rarContent = readRARContent(data.map(function(d) {\n return {\n name: d.name,\n content: new Uint8Array(d.content)\n }\n }), password, cb)\n let rec = function(entry) {\n if (!entry) return;\n if (entry.type === \'file\') {\n postMessage({"t":2,"file":entry.fullFileName,"size":entry.fileSize,"data":entry.fileContent});\n } else if (entry.type === \'dir\') {\n Object.keys(entry.ls).forEach(function(k) {\n rec(entry.ls[k]);\n })\n } else {\n throw "Unknown type";\n }\n }\n rec(rarContent);\n postMessage({"t":1});\n return rarContent;\n};\nonmessage = function(data) {\n dataToPass.push({name: \'test.rar\', content: data.data});\n};\n ';
const blob = new Blob([data], {
type: 'application/javascript'
})
resolve(blob);
}, null, false, {responseType: "arraybuffer", method: "GET"});
} else {
const blob = new Blob([res.data], {
type: 'application/javascript'
})
this.cache[obj] = blob;
resolve(blob);
}
}, null, false, {responseType: "arraybuffer", method: "GET"});
})
}
decompressFile(method, data, updateMsg, fileCbFunc) {
return new Promise(async callback => {
const file = await this.getWorkerFile(method);
const worker = new Worker(URL.createObjectURL(file));
const files = {};
worker.onmessage = (data) => {
if (!data.data) return;
//data.data.t/ 4=progress, 2 is file, 1 is zip done
if (data.data.t === 4) {
const pg = data.data;
const num = Math.floor(pg.current / pg.total * 100);
if (isNaN(num)) return;
const progress = ' '+num.toString()+'%';
updateMsg(progress, true);
}
if (data.data.t === 2) {
if (typeof fileCbFunc === "function") {
fileCbFunc(data.data.file, data.data.data);
files[data.data.file] = true;
} else {
files[data.data.file] = data.data.data;
}
}
if (data.data.t === 1) {
callback(files);
}
}
worker.postMessage(data);
});
}
}
window.EJS_COMPRESSION = EJS_COMPRESSION;

View file

@ -202,20 +202,29 @@ class EmulatorJS {
})
}
checkForUpdates() {
if (this.ejs_version.endsWith("-beta")) {
console.warn("Using EmulatorJS beta. Not checking for updates. This instance may be out of date. Using stable is highly recommended unless you build and ship your own cores.");
return;
}
fetch('https://cdn.emulatorjs.org/stable/data/version.json').then(response => {
if (response.ok) {
response.text().then(body => {
let version = JSON.parse(body);
if (this.ejs_num_version < version.current_version) {
console.log('Using EmulatorJS version ' + this.ejs_num_version + ' but the newest version is ' + version.current_version + '\nopen https://github.com/EmulatorJS/EmulatorJS to update');
if (this.versionAsInt(this.ejs_version) < this.versionAsInt(version.current_version)) {
console.log('Using EmulatorJS version ' + this.versionAsInt(this.ejs_version) + ' but the newest version is ' + this.versionAsInt(version.current_version) + '\nopen https://github.com/EmulatorJS/EmulatorJS to update');
}
})
}
})
}
versionAsInt(ver) {
if (ver.endsWith("-beta")) {
return 99999999;
}
return parseInt(ver.split(".").join(""));
}
constructor(element, config) {
this.ejs_version = "4.0.12";
this.ejs_num_version = 401.2;
this.ejs_version = "4.0.13-beta";
this.debug = (window.EJS_DEBUG_XX === true);
if (this.debug || (window.location && ['localhost', '127.0.0.1'].includes(location.hostname))) this.checkForUpdates();
this.netplayEnabled = (window.EJS_DEBUG_XX === true) && (window.EJS_EXPERIMENTAL_NETPLAY === true);
@ -475,126 +484,16 @@ class EmulatorJS {
}
return text;
}
isCompressed(data) { //https://www.garykessler.net/library/file_sigs.html
//todo. Use hex instead of numbers
if ((data[0] === 80 && data[1] === 75) && ((data[2] === 3 && data[3] === 4) || (data[2] === 5 && data[3] === 6) || (data[2] === 7 && data[3] === 8))) {
return 'zip';
} else if (data[0] === 55 && data[1] === 122 && data[2] === 188 && data[3] === 175 && data[4] === 39 && data[5] === 28) {
return '7z';
} else if ((data[0] === 82 && data[1] === 97 && data[2] === 114 && data[3] === 33 && data[4] === 26 && data[5] === 7) && ((data[6] === 0) || (data[6] === 1 && data[7] == 0))) {
return 'rar';
}
}
checkCompression(data, msg, fileCbFunc) {
if (!this.compression) {
this.compression = new window.EJS_COMPRESSION(this);
}
if (msg) {
this.textElem.innerText = msg;
}
//to be put in another file
const createWorker = (path) => {
return new Promise((resolve, reject) => {
this.downloadFile(path, (res) => {
if (res === -1) {
this.startGameError(this.localization('Network Error'));
return;
}
const blob = new Blob([res.data], {
'type': 'application/javascript'
})
const url = window.URL.createObjectURL(blob);
resolve(new Worker(url));
}, null, false, {responseType: "arraybuffer", method: "GET"});
})
}
const files = {};
let res;
const onMessage = (data) => {
if (!data.data) return;
//data.data.t/ 4=progress, 2 is file, 1 is zip done
if (data.data.t === 4 && msg) {
const pg = data.data;
const num = Math.floor(pg.current / pg.total * 100);
if (isNaN(num)) return;
const progress = ' '+num.toString()+'%';
this.textElem.innerText = msg + progress;
}
if (data.data.t === 2) {
if (typeof fileCbFunc === "function") {
fileCbFunc(data.data.file, data.data.data);
files[data.data.file] = true;
} else {
files[data.data.file] = data.data.data;
}
}
if (data.data.t === 1) {
res(files);
}
}
const decompress7z = (file) => {
return new Promise((resolve, reject) => {
res = resolve;
createWorker('compression/extract7z.js').then((worker) => {
worker.onmessage = onMessage;
worker.postMessage(file);
//console.log(file);
})
})
}
const decompressRar = (file) => {
return new Promise((resolve, reject) => {
res = resolve;
this.downloadFile("compression/libunrar.js", (res) => {
this.downloadFile("compression/libunrar.wasm", (res2) => {
if (res === -1 || res2 === -1) {
this.startGameError(this.localization('Network Error'));
return;
}
const path = URL.createObjectURL(new Blob([res2.data], {type: "application/wasm"}));
let data = '\nlet dataToPass = [];\nModule = {\n monitorRunDependencies: function(left) {\n if (left == 0) {\n setTimeout(function() {\n unrar(dataToPass, null);\n }, 100);\n }\n },\n onRuntimeInitialized: function() {\n },\n locateFile: function(file) {\n return \''+path+'\';\n }\n};\n'+res.data+'\nlet unrar = function(data, password) {\n let cb = function(fileName, fileSize, progress) {\n postMessage({"t":4,"current":progress,"total":fileSize, "name": fileName});\n };\n\n let rarContent = readRARContent(data.map(function(d) {\n return {\n name: d.name,\n content: new Uint8Array(d.content)\n }\n }), password, cb)\n let rec = function(entry) {\n if (!entry) return;\n if (entry.type === \'file\') {\n postMessage({"t":2,"file":entry.fullFileName,"size":entry.fileSize,"data":entry.fileContent});\n } else if (entry.type === \'dir\') {\n Object.keys(entry.ls).forEach(function(k) {\n rec(entry.ls[k]);\n })\n } else {\n throw "Unknown type";\n }\n }\n rec(rarContent);\n postMessage({"t":1});\n return rarContent;\n};\nonmessage = function(data) {\n dataToPass.push({name: \'test.rar\', content: data.data});\n};\n ';
const blob = new Blob([data], {
'type': 'application/javascript'
})
const url = window.URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = onMessage;
worker.postMessage(file);
}, null, false, {responseType: "arraybuffer", method: "GET"})
}, null, false, {responseType: "text", method: "GET"});
})
}
const decompressZip = (file) => {
return new Promise((resolve, reject) => {
res = resolve;
createWorker('compression/extractzip.js').then((worker) => {
worker.onmessage = onMessage;
worker.postMessage(file);
})
})
}
const compression = this.isCompressed(data.slice(0, 10));
if (compression) {
//Need to do zip and rar still
if (compression === "7z") {
return decompress7z(data);
} else if (compression === "zip") {
return decompressZip(data);
} else if (compression === "rar") {
return decompressRar(data);
}
} else {
if (typeof fileCbFunc === "function") {
fileCbFunc("!!notCompressedData", data);
return new Promise(resolve => resolve({"!!notCompressedData": true}));
} else {
return new Promise(resolve => resolve({"!!notCompressedData": data}));
}
}
}
versionAsInt(ver) {
return parseInt(ver.split(".").join(""));
return this.compression.decompress(data, (m, appendMsg) => {
this.textElem.innerText = appendMsg ? (msg + m) : m;
}, fileCbFunc);
}
checkCoreCompatibility(version) {
// Leave commented until next release - this is ready to go.
@ -662,11 +561,11 @@ class EmulatorJS {
let legacy = (this.supportsWebgl2 && this.webgl2Enabled ? "" : "-legacy");
let filename = this.getCore()+(this.config.threads ? "-thread" : "")+legacy+"-wasm.data";
this.storage.core.get(filename).then((result) => {
if (result && result.version === rep.buildStart && !this.debug) {
gotCore(result.data);
return;
}
const corePath = 'cores/'+filename;
if (result && result.version === rep.buildStart && !this.debug) {
gotCore(result.data);
return;
}
const corePath = 'cores/'+filename;
this.downloadFile(corePath, (res) => {
if (res === -1) {
console.log("File not found, attemping to fetch from emulatorjs cdn");
@ -702,9 +601,11 @@ class EmulatorJS {
}, null, false, {responseType: "text", method: "GET"});
}
initGameCore(js, wasm, thread) {
this.initModule(wasm, thread);
let script = this.createElement("script");
script.src = URL.createObjectURL(new Blob([js], {type: "application/javascript"}));
script.addEventListener("load", () => {
this.initModule(wasm, thread);
});
document.body.appendChild(script);
}
getBaseFileName(force) {
@ -773,11 +674,11 @@ class EmulatorJS {
this.checkCompression(new Uint8Array(data), this.localization("Decompress Game Patch")).then((data) => {
for (const k in data) {
if (k === "!!notCompressedData") {
FS.writeFile(this.config.gamePatchUrl.split('/').pop().split("#")[0].split("?")[0], data[k]);
this.gameManager.FS.writeFile(this.config.gamePatchUrl.split('/').pop().split("#")[0].split("?")[0], data[k]);
break;
}
if (k.endsWith('/')) continue;
FS.writeFile("/" + k.split('/').pop(), data[k]);
this.gameManager.FS.writeFile("/" + k.split('/').pop(), data[k]);
}
resolve();
})
@ -823,11 +724,11 @@ class EmulatorJS {
this.checkCompression(new Uint8Array(data), this.localization("Decompress Game Parent")).then((data) => {
for (const k in data) {
if (k === "!!notCompressedData") {
FS.writeFile(this.config.gameParentUrl.split('/').pop().split("#")[0].split("?")[0], data[k]);
this.gameManager.FS.writeFile(this.config.gameParentUrl.split('/').pop().split("#")[0].split("?")[0], data[k]);
break;
}
if (k.endsWith('/')) continue;
FS.writeFile("/" + k.split('/').pop(), data[k]);
this.gameManager.FS.writeFile("/" + k.split('/').pop(), data[k]);
}
resolve();
})
@ -873,11 +774,11 @@ class EmulatorJS {
this.checkCompression(new Uint8Array(data), this.localization("Decompress Game BIOS")).then((data) => {
for (const k in data) {
if (k === "!!notCompressedData") {
FS.writeFile(this.config.biosUrl.split('/').pop().split("#")[0].split("?")[0], data[k]);
this.gameManager.FS.writeFile(this.config.biosUrl.split('/').pop().split("#")[0].split("?")[0], data[k]);
break;
}
if (k.endsWith('/')) continue;
FS.writeFile("/" + k.split('/').pop(), data[k]);
this.gameManager.FS.writeFile("/" + k.split('/').pop(), data[k]);
}
resolve();
})
@ -928,7 +829,7 @@ class EmulatorJS {
const gotGameData = (data) => {
if (['arcade', 'mame'].includes(this.getCore(true))) {
this.fileName = this.getBaseFileName(true);
FS.writeFile(this.fileName, new Uint8Array(data));
this.gameManager.FS.writeFile(this.fileName, new Uint8Array(data));
resolve();
return;
}
@ -950,20 +851,20 @@ class EmulatorJS {
for (let i=0; i<paths.length-1; i++) {
if (paths[i] === "") continue;
cp += `/${paths[i]}`;
if (!FS.analyzePath(cp).exists) {
FS.mkdir(cp);
if (!this.gameManager.FS.analyzePath(cp).exists) {
this.gameManager.FS.mkdir(cp);
}
}
}
if (fileName.endsWith('/')) {
FS.mkdir(fileName);
this.gameManager.FS.mkdir(fileName);
return;
}
if (fileName === "!!notCompressedData") {
FS.writeFile(altName, fileData);
this.gameManager.FS.writeFile(altName, fileData);
fileNames.push(altName);
} else {
FS.writeFile(`/${fileName}`, fileData);
this.gameManager.FS.writeFile(`/${fileName}`, fileData);
fileNames.push(fileName);
}
}).then(() => {
@ -1067,26 +968,28 @@ class EmulatorJS {
})();
}
initModule(wasmData, threadData) {
window.Module = {
'noInitialRun': true,
'onRuntimeInitialized': this.downloadFiles.bind(this),
'arguments': [],
'preRun': [],
'postRun': [],
'canvas': this.canvas,
'print': (msg) => {
window.EJS_Runtime({
noInitialRun: true,
onRuntimeInitialized: null,
arguments: [],
preRun: [],
postRun: [],
canvas: this.canvas,
print: (msg) => {
if (this.debug) {
console.log(msg);
}
},
'printErr': (msg) => {
printErr: (msg) => {
if (this.debug) {
console.log(msg);
}
},
'totalDependencies': 0,
'monitorRunDependencies': () => {},
'locateFile': function(fileName) {
totalDependencies: 0,
monitorRunDependencies: () => {},
locateFile: function(fileName) {
if (this.debug) console.log(fileName);
if (fileName.endsWith(".wasm")) {
return URL.createObjectURL(new Blob([wasmData], {type: "application/wasm"}));
@ -1094,8 +997,10 @@ class EmulatorJS {
return URL.createObjectURL(new Blob([threadData], {type: "application/javascript"}));
}
}
};
this.Module = window.Module;
}).then(module => {
this.Module = module;
this.downloadFiles();
});
}
startGame() {
try {
@ -1686,19 +1591,18 @@ class EmulatorJS {
const restartButton = addButton("Restart", '<svg viewBox="0 0 512 512"><path d="M496 48V192c0 17.69-14.31 32-32 32H320c-17.69 0-32-14.31-32-32s14.31-32 32-32h63.39c-29.97-39.7-77.25-63.78-127.6-63.78C167.7 96.22 96 167.9 96 256s71.69 159.8 159.8 159.8c34.88 0 68.03-11.03 95.88-31.94c14.22-10.53 34.22-7.75 44.81 6.375c10.59 14.16 7.75 34.22-6.375 44.81c-39.03 29.28-85.36 44.86-134.2 44.86C132.5 479.9 32 379.4 32 256s100.5-223.9 223.9-223.9c69.15 0 134 32.47 176.1 86.12V48c0-17.69 14.31-32 32-32S496 30.31 496 48z"/></svg>', () => {
if (this.isNetplay && this.netplay.owner) {
this.gameManager.saveSaveFiles();
this.gameManager.restart();
this.netplay.reset();
this.netplay.sendMessage({restart:true});
this.play();
} else if (!this.isNetplay) {
this.gameManager.saveSaveFiles();
this.gameManager.restart();
}
});
const pauseButton = addButton("Pause", '<svg viewBox="0 0 320 512"><path d="M272 63.1l-32 0c-26.51 0-48 21.49-48 47.1v288c0 26.51 21.49 48 48 48L272 448c26.51 0 48-21.49 48-48v-288C320 85.49 298.5 63.1 272 63.1zM80 63.1l-32 0c-26.51 0-48 21.49-48 48v288C0 426.5 21.49 448 48 448l32 0c26.51 0 48-21.49 48-48v-288C128 85.49 106.5 63.1 80 63.1z"/></svg>', () => {
if (this.isNetplay && this.netplay.owner) {
this.pause();
this.gameManager.saveSaveFiles();
this.netplay.sendMessage({pause:true});
} else if (!this.isNetplay) {
this.pause();
@ -1816,10 +1720,10 @@ class EmulatorJS {
for (let i=0; i<paths.length-1; i++) {
if (paths[i] === "") continue;
cp += "/"+paths[i];
if (!FS.analyzePath(cp).exists) FS.mkdir(cp);
if (!this.gameManager.FS.analyzePath(cp).exists) this.gameManager.FS.mkdir(cp);
}
if (FS.analyzePath(path).exists) FS.unlink(path);
FS.writeFile(path, sav);
if (this.gameManager.FS.analyzePath(path).exists) this.gameManager.FS.unlink(path);
this.gameManager.FS.writeFile(path, sav);
this.gameManager.loadSaveFiles();
});
const netplay = addButton("Netplay", '<svg viewBox="0 0 512 512"><path fill="currentColor" d="M364.215 192h131.43c5.439 20.419 8.354 41.868 8.354 64s-2.915 43.581-8.354 64h-131.43c5.154-43.049 4.939-86.746 0-128zM185.214 352c10.678 53.68 33.173 112.514 70.125 151.992.221.001.44.008.661.008s.44-.008.661-.008c37.012-39.543 59.467-98.414 70.125-151.992H185.214zm174.13-192h125.385C452.802 84.024 384.128 27.305 300.95 12.075c30.238 43.12 48.821 96.332 58.394 147.925zm-27.35 32H180.006c-5.339 41.914-5.345 86.037 0 128h151.989c5.339-41.915 5.345-86.037-.001-128zM152.656 352H27.271c31.926 75.976 100.6 132.695 183.778 147.925-30.246-43.136-48.823-96.35-58.393-147.925zm206.688 0c-9.575 51.605-28.163 104.814-58.394 147.925 83.178-15.23 151.852-71.949 183.778-147.925H359.344zm-32.558-192c-10.678-53.68-33.174-112.514-70.125-151.992-.221 0-.44-.008-.661-.008s-.44.008-.661.008C218.327 47.551 195.872 106.422 185.214 160h141.572zM16.355 192C10.915 212.419 8 233.868 8 256s2.915 43.581 8.355 64h131.43c-4.939-41.254-5.154-84.951 0-128H16.355zm136.301-32c9.575-51.602 28.161-104.81 58.394-147.925C127.872 27.305 59.198 84.024 27.271 160h125.385z"/></svg>', async () => {
@ -2008,6 +1912,56 @@ class EmulatorJS {
}
}
const exitEmulation = addButton("Exit EmulatorJS", '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460 460"><path style="fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(255,255,255);stroke-opacity:1;stroke-miterlimit:4;" d="M 14.000061 7.636414 L 14.000061 4.5 C 14.000061 4.223877 13.776123 3.999939 13.5 3.999939 L 4.5 3.999939 C 4.223877 3.999939 3.999939 4.223877 3.999939 4.5 L 3.999939 19.5 C 3.999939 19.776123 4.223877 20.000061 4.5 20.000061 L 13.5 20.000061 C 13.776123 20.000061 14.000061 19.776123 14.000061 19.5 L 14.000061 16.363586 " transform="matrix(21.333333,0,0,21.333333,0,0)"/><path style="fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(255,255,255);stroke-opacity:1;stroke-miterlimit:4;" d="M 9.999939 12 L 21 12 M 21 12 L 18.000366 8.499939 M 21 12 L 18 15.500061 " transform="matrix(21.333333,0,0,21.333333,0,0)"/></svg>', async () => {
const popups = this.createSubPopup();
this.game.appendChild(popups[0]);
popups[1].classList.add("ejs_cheat_parent");
popups[1].style.width = "100%";
const popup = popups[1];
const header = this.createElement("div");
header.classList.add("ejs_cheat_header");
const title = this.createElement("h2");
title.innerText = this.localization("Are you sure you want to exit?");
title.classList.add("ejs_cheat_heading");
const close = this.createElement("button");
close.classList.add("ejs_cheat_close");
header.appendChild(title);
header.appendChild(close);
popup.appendChild(header);
this.addEventListener(close, "click", (e) => {
popups[0].remove();
})
popup.appendChild(this.createElement("br"));
const footer = this.createElement("footer");
const submit = this.createElement("button");
const closeButton = this.createElement("button");
submit.innerText = this.localization("Exit");
closeButton.innerText = this.localization("Cancel");
submit.classList.add("ejs_button_button");
closeButton.classList.add("ejs_button_button");
submit.classList.add("ejs_popup_submit");
closeButton.classList.add("ejs_popup_submit");
submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)";
footer.appendChild(submit);
const span = this.createElement("span");
span.innerText = " ";
footer.appendChild(span);
footer.appendChild(closeButton);
popup.appendChild(footer);
this.addEventListener(closeButton, "click", (e) => {
popups[0].remove();
})
this.addEventListener(submit, "click", (e) => {
popups[0].remove();
const body = this.createPopup("EmulatorJS has exited", {});
this.callEvent("exit");
})
});
this.addEventListener(document, "webkitfullscreenchange mozfullscreenchange fullscreenchange", (e) => {
if (e.target !== this.elements.parent) return;

View file

@ -1 +1 @@
{ "current_version": 401.2 }
{ "current_version": "4.0.12" }