mirror of
https://github.com/alexankitty/Myrient-Search-Engine.git
synced 2026-01-15 08:23:18 -03:00
Initial implementation of the web emulator
This commit is contained in:
4
.env
4
.env
@@ -10,4 +10,6 @@ MAX_JOB_QUEUE=1000
|
||||
# Changes the maximum number of pages that can be fetched for parsing. Has a massive impact on memory usage. Setting to 12 results in about 1.1GiB memory usage
|
||||
MAX_FETCH_JOBS=1000
|
||||
# Changes the name of your instance
|
||||
INSTANCE_NAME=Myrient
|
||||
INSTANCE_NAME=Myrient
|
||||
# Enable the built-in emulator
|
||||
EMULATOR_ENABLED=true
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
- MAX_JOB_QUEUE=1000
|
||||
- MAX_FETCH_JOBS=1000
|
||||
- INSTANCE_NAME=Myrient
|
||||
- EMULATOR_ENABLED=true
|
||||
volumes:
|
||||
- ./data:/usr/src/app/data
|
||||
restart: unless-stopped
|
||||
69
lib/emulatorConfig.js
Normal file
69
lib/emulatorConfig.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// See https://emulatorjs.org/docs/systems for available cores
|
||||
const coreMap = {
|
||||
// Nintendo Systems
|
||||
'Nintendo Entertainment System': 'fceumm',
|
||||
'Super Nintendo Entertainment System': 'snes9x',
|
||||
'Nintendo 64': 'mupen64plus_next',
|
||||
'Nintendo DS': 'desmume2015',
|
||||
'Nintendo Game Boy': 'gambatte',
|
||||
'Nintendo Game Boy Color': 'gambatte',
|
||||
'Nintendo Game Boy Advance': 'mgba',
|
||||
|
||||
// Sega Systems
|
||||
'Sega Master System': 'smsplus',
|
||||
'Sega Game Gear': 'genesis_plus_gx', // TODO: fix rom loading
|
||||
'Sega Mega Drive': 'genesis_plus_gx',
|
||||
'Sega CD': 'genesis_plus_gx', // TODO: add bios
|
||||
'Sega 32X': 'picodrive', // Known issue: https://github.com/EmulatorJS/EmulatorJS/issues/579
|
||||
'Sega Saturn': 'yabause',
|
||||
|
||||
// Atari Systems
|
||||
'Atari 2600': 'stella2014',
|
||||
'Atari 5200': 'a5200',
|
||||
'Atari 7800': 'prosystem',
|
||||
'Atari Jaguar': 'virtualjaguar',
|
||||
'Atari Lynx': 'handy',
|
||||
|
||||
// Commodore Systems
|
||||
'Commodore 64': 'vice_x64sc',
|
||||
'Commodore 128': 'vice_x128', // Untested, Myrient has no ROMs for it
|
||||
'Commodore Amiga': 'puae', // TODO: fix rom loading
|
||||
'Commodore PET': 'vice_xpet', // Untested, Myrient has no ROMs for it
|
||||
'Commodore Plus-4': 'vice_xplus4', // TODO: fix rom loading
|
||||
'Commodore VIC-20': 'vice_xvic', // TODO: fix rom loading
|
||||
|
||||
// Sony Systems
|
||||
'Sony PlayStation 1': 'pcsx_rearmed', // TODO: fix rom loading
|
||||
|
||||
// Other Systems
|
||||
'Arcade': 'fbneo', // TODO: fix rom loading
|
||||
'ColecoVision': 'gearcoleco', // TODO: add bios
|
||||
'Panasonic 3DO': 'opera', // TODO: fix rom loading
|
||||
};
|
||||
|
||||
const COMPATIBLE_SYSTEMS = Object.keys(coreMap);
|
||||
|
||||
export function isEmulatorCompatible(category) {
|
||||
if (process.env.EMULATOR_ENABLED !== 'true') {
|
||||
return false;
|
||||
}
|
||||
return COMPATIBLE_SYSTEMS.includes(category);
|
||||
}
|
||||
|
||||
export function getEmulatorConfig(category) {
|
||||
const core = coreMap[category] || 'unknown';
|
||||
|
||||
// Add system-specific settings
|
||||
const config = {
|
||||
core,
|
||||
system: category,
|
||||
options: {}
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function isNonGameContent(filename, nonGameTerms) {
|
||||
const pattern = new RegExp(nonGameTerms.terms.join('|'), 'i');
|
||||
return pattern.test(filename);
|
||||
}
|
||||
56
lib/nonGameTerms.json
Normal file
56
lib/nonGameTerms.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"terms": [
|
||||
"7z",
|
||||
"addon",
|
||||
"artwork",
|
||||
"audio",
|
||||
"beta",
|
||||
"box",
|
||||
"boxart",
|
||||
"cheat",
|
||||
"config",
|
||||
"cfg",
|
||||
"csv",
|
||||
"debug",
|
||||
"dlc",
|
||||
"document",
|
||||
"driver",
|
||||
"editor",
|
||||
"emulator",
|
||||
"expansion",
|
||||
"firmware",
|
||||
"guide",
|
||||
"hack",
|
||||
"html",
|
||||
"ini",
|
||||
"installer",
|
||||
"intro",
|
||||
"json",
|
||||
"manual",
|
||||
"mod",
|
||||
"movie",
|
||||
"music",
|
||||
"ost",
|
||||
"overlay",
|
||||
"patch",
|
||||
"plugin",
|
||||
"preview",
|
||||
"readme",
|
||||
"rom",
|
||||
"screenshot",
|
||||
"sample",
|
||||
"save",
|
||||
"savestate",
|
||||
"sdk",
|
||||
"setup",
|
||||
"soundtrack",
|
||||
"terms",
|
||||
"tool",
|
||||
"trainer",
|
||||
"txt",
|
||||
"update",
|
||||
"utility",
|
||||
"video",
|
||||
"wallpaper"
|
||||
]
|
||||
}
|
||||
100
package-lock.json
generated
100
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"file-older-than": "^1.0.0",
|
||||
"innertext": "^1.0.3",
|
||||
"jsdom": "^25.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"minisearch": "^7.1.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"node-fetch": "^3.3.2",
|
||||
@@ -597,6 +598,12 @@
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
@@ -1272,6 +1279,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -1302,6 +1315,12 @@
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
||||
@@ -1360,6 +1379,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -1569,6 +1609,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz",
|
||||
@@ -1605,6 +1651,12 @@
|
||||
"@napi-rs/nice": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1666,6 +1718,27 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
||||
@@ -1791,6 +1864,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -1824,6 +1903,21 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -1915,6 +2009,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"node-html-parser": "^6.1.13",
|
||||
"piscina": "^4.7.0",
|
||||
"sanitize": "^2.1.2",
|
||||
"figlet": "^1.7.0"
|
||||
"figlet": "^1.7.0",
|
||||
"jszip": "^3.10.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
63
server.js
63
server.js
@@ -10,13 +10,17 @@ import sanitize from "sanitize";
|
||||
import debugPrint from "./lib/debugprint.js";
|
||||
import compression from "compression";
|
||||
import { generateAsciiArt } from './lib/asciiart.js';
|
||||
import { getEmulatorConfig, isEmulatorCompatible, isNonGameContent } from './lib/emulatorConfig.js';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
let fileListPath = "./data/filelist.json";
|
||||
let queryCountFile = "./data/queries.txt";
|
||||
let categoryListPath = "./lib/categories.json"
|
||||
let searchAlikesPath = './lib/searchalikes.json'
|
||||
let nonGameTermsPath = './lib/nonGameTerms.json'
|
||||
let categoryList = await FileHandler.parseJsonFile(categoryListPath);
|
||||
global.searchAlikes = await FileHandler.parseJsonFile(searchAlikesPath)
|
||||
let nonGameTerms = await FileHandler.parseJsonFile(nonGameTermsPath);
|
||||
let crawlTime = 0;
|
||||
let queryCount = 0;
|
||||
let fileCount = 0;
|
||||
@@ -101,7 +105,8 @@ let defaultOptions = {
|
||||
queryCount: queryCount,
|
||||
fileCount: fileCount,
|
||||
termCount: search.miniSearch.termCount,
|
||||
generateAsciiArt: generateAsciiArt
|
||||
generateAsciiArt: generateAsciiArt,
|
||||
isEmulatorCompatible: isEmulatorCompatible
|
||||
};
|
||||
|
||||
function updateDefaults(){
|
||||
@@ -216,6 +221,62 @@ app.get("/about", function (req, res) {
|
||||
res.render(indexPage, buildOptions(page));
|
||||
});
|
||||
|
||||
app.get("/play/:id", async function (req, res) {
|
||||
// Block access if emulator is disabled
|
||||
if (process.env.EMULATOR_ENABLED !== 'true') {
|
||||
res.redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
let fileId = parseInt(req.params.id);
|
||||
let romFile = search.findIndex(fileId);
|
||||
|
||||
if (!romFile) {
|
||||
res.redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
let options = {
|
||||
romFile: romFile,
|
||||
emulatorConfig: getEmulatorConfig(romFile.category),
|
||||
isNonGame: isNonGameContent(romFile.filename, nonGameTerms)
|
||||
};
|
||||
|
||||
let page = "emulator";
|
||||
options = buildOptions(page, options);
|
||||
res.render(indexPage, options);
|
||||
});
|
||||
|
||||
app.get("/proxy-rom/:id", async function (req, res) {
|
||||
// Block access if emulator is disabled
|
||||
if (process.env.EMULATOR_ENABLED !== 'true') {
|
||||
res.status(403).send('Emulator feature is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
let fileId = parseInt(req.params.id);
|
||||
let romFile = search.findIndex(fileId);
|
||||
|
||||
if (!romFile) {
|
||||
res.status(404).send('ROM not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(romFile.path);
|
||||
const contentLength = response.headers.get('content-length');
|
||||
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Length', contentLength);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${romFile.filename}"`);
|
||||
|
||||
response.body.pipe(res);
|
||||
} catch (error) {
|
||||
console.error('Error proxying ROM:', error);
|
||||
res.status(500).send('Error fetching ROM');
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(process.env.PORT, process.env.BIND_ADDRESS);
|
||||
server.on("listening", function () {
|
||||
console.log(
|
||||
|
||||
@@ -18,6 +18,32 @@
|
||||
<a href="https://myrient.erista.me/donate/" class="btn btn-secondary">Donate to Myrient</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 border-top pt-3">
|
||||
<h5>Built-in Emulator</h5>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<p>This website includes a built-in emulator powered by <a href="https://emulatorjs.org/">EmulatorJS</a> that brings retro gaming directly to your browser.</p>
|
||||
<p>Compatible games will feature a play button on their search result page.</p>
|
||||
|
||||
<p>For the best gaming experience, use a Chromium-based browser with hardware acceleration turned on.</p>
|
||||
|
||||
<p class="text-secondary">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Games are loaded directly from Myrient's public archive. Save states are stored locally in the browser.
|
||||
</small>
|
||||
<br>
|
||||
<small>
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
ROM hacks, soundtracks, and other non-game content are not supported by the emulator and may fail to load.
|
||||
</small>
|
||||
</p>
|
||||
|
||||
<% } else { %>
|
||||
<p>Web Emulator functionality was disabled by the administrator.</p>
|
||||
<p>Contact the administrator or spin up your own instance of <%= process.env.INSTANCE_NAME || 'Myrient' %> Search.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="border-top pt-3">
|
||||
<p>Search engine created by <a href="https://github.com/alexankitty">Alexankitty</a></p>
|
||||
<p><a href="https://github.com/alexankitty/myrient-global-search">View project on GitHub</a></p>
|
||||
|
||||
223
views/pages/emulator.ejs
Normal file
223
views/pages/emulator.ejs
Normal file
@@ -0,0 +1,223 @@
|
||||
<div class="container-fluid">
|
||||
<!-- Header with game info -->
|
||||
<div class="row mb-4 mt-3">
|
||||
<div class="col-12 text-center">
|
||||
<h2 class="text-white"><%= romFile.filename.replace(/\.[^/.]+$/, '') %></h2>
|
||||
<p class="text-secondary"><%= romFile.category %></p>
|
||||
|
||||
<% if (isNonGame) { %>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Warning: This file may not be a game ROM and might not work properly in the web emulator.
|
||||
See the <a href="/about" class="alert-link">About</a> page for more information.
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main game container with proper padding and height -->
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8">
|
||||
<div id="game-wrapper" class="position-relative">
|
||||
<div id="game" class="w-100"></div>
|
||||
<!-- Progress bar inside game container -->
|
||||
<div id="progress-container" class="progress-overlay">
|
||||
<div class="progress" style="height: 25px; background-color: #2a2a2a; width: 80%; max-width: 500px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: 0%"
|
||||
id="download-progress">
|
||||
<span id="progress-text">Loading ROM...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disclaimer footer -->
|
||||
<div class="row mt-4 mb-3">
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
This emulator loads ROMs directly from <a href="https://myrient.erista.me/" class="alert-link">Myrient</a> and is not affiliated with them.
|
||||
See the <a href="/about" class="alert-link">About</a> page for more information.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Only keep basic container styling */
|
||||
#game-wrapper {
|
||||
padding-top: 0;
|
||||
background: #222;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Keep only the aspect ratio for the game container */
|
||||
#game {
|
||||
aspect-ratio: 4/3;
|
||||
max-height: 700px;
|
||||
}
|
||||
|
||||
/* Keep alert styling for consistency */
|
||||
.alert-secondary {
|
||||
background-color: #2a2a2a;
|
||||
border-color: #3a3a3a;
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
.alert-secondary a.alert-link {
|
||||
color: #bbb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.alert-secondary a.alert-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.progress-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: none; /* Hidden by default */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.progress {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#progress-text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
|
||||
<script>
|
||||
// Display important notice immediately
|
||||
console.log('%cAbout this Page', 'font-size: 20px; font-weight: bold; color: #4CAF50;');
|
||||
console.log(
|
||||
'%cThis page contains a game emulator that provides access to games through Myrient\'s public archive.\n' +
|
||||
'We are not affiliated with or endorsed by Myrient.\n' +
|
||||
'Visit the About page for more details on how this integration works and other important information.',
|
||||
'font-size: 14px; color: #90CAF9;'
|
||||
);
|
||||
console.log(`%c${window.location.origin}/about`, 'font-size: 14px; color: #90CAF9;');
|
||||
|
||||
// Configure EmulatorJS
|
||||
window.EJS_player = '#game';
|
||||
window.EJS_core = '<%= emulatorConfig.core %>';
|
||||
window.EJS_gameUrl = '/proxy-rom/<%= romFile.id %>';
|
||||
window.EJS_pathtodata = 'https://cdn.emulatorjs.org/stable/data/';
|
||||
window.EJS_startOnLoaded = true;
|
||||
window.EJS_biosUrl = undefined;
|
||||
window.EJS_gameName = '<%= romFile.filename.replace(/\.[^/.]+$/, '') %>';
|
||||
window.EJS_backgroundBlur = true;
|
||||
window.EJS_defaultOptions = {
|
||||
'save-state-slot': 1,
|
||||
'save-state-location': 'local'
|
||||
};
|
||||
|
||||
// Handle ROMs with progress
|
||||
async function loadRom() {
|
||||
try {
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('download-progress');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
progressContainer.style.display = 'flex';
|
||||
|
||||
// Check if the file is compressed based on extension
|
||||
const isCompressed = /\.(zip)$/i.test('<%= romFile.filename %>');
|
||||
|
||||
// Download phase
|
||||
progressText.textContent = 'Downloading ROM...';
|
||||
const response = await fetch(EJS_gameUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!isCompressed) {
|
||||
// For uncompressed ROMs, just return the URL directly
|
||||
progressContainer.style.display = 'none';
|
||||
return EJS_gameUrl;
|
||||
}
|
||||
|
||||
// For compressed files, continue with decompression
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const total = parseInt(contentLength, 10);
|
||||
let loaded = 0;
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const chunks = [];
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
chunks.push(value);
|
||||
loaded += value.length;
|
||||
|
||||
const percent = Math.round((loaded / total) * 100);
|
||||
progressBar.style.width = percent + '%';
|
||||
progressText.textContent = `Downloading ROM: ${percent}%`;
|
||||
}
|
||||
|
||||
// Decompression phase - keep progress at 100%
|
||||
progressText.textContent = 'Decompressing the game...';
|
||||
|
||||
const blob = new Blob(chunks);
|
||||
const zip = await JSZip.loadAsync(blob);
|
||||
const files = Object.keys(zip.files);
|
||||
|
||||
const romFile = files.find(f => !zip.files[f].dir);
|
||||
if (!romFile) {
|
||||
throw new Error('No ROM file found in ZIP archive');
|
||||
}
|
||||
|
||||
const romData = await zip.files[romFile].async('blob');
|
||||
progressContainer.style.display = 'none';
|
||||
return URL.createObjectURL(romData);
|
||||
} catch (error) {
|
||||
console.error('Error loading ROM:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
loadRom()
|
||||
.then(romUrl => {
|
||||
window.EJS_gameUrl = romUrl;
|
||||
const script = document.createElement('script');
|
||||
script.src = `${window.EJS_pathtodata}loader.js`;
|
||||
document.body.appendChild(script);
|
||||
})
|
||||
.catch(error => {
|
||||
const gameDiv = document.getElementById('game');
|
||||
gameDiv.innerHTML = `<div class="alert alert-danger">
|
||||
Error loading game: ${error.message}
|
||||
</div>`;
|
||||
});
|
||||
</script>
|
||||
@@ -45,6 +45,9 @@
|
||||
<th>Size</th>
|
||||
<th>Date</th>
|
||||
<th>Search Score</th>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<th>Play</th>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<% for (let x = entryStart; x < entryEnd; x++) { %>
|
||||
@@ -72,6 +75,15 @@
|
||||
<td>
|
||||
<%= results.items[x].score.toFixed(2) %>
|
||||
</td>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<td>
|
||||
<% if (isEmulatorCompatible(results.items[x].category)) { %>
|
||||
<a href="/play/<%= results.items[x].id %>" class="btn btn-sm btn-secondary">Play</a>
|
||||
<% } else { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled>----</button>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
@@ -128,7 +140,19 @@
|
||||
</div>
|
||||
<script defer>
|
||||
resultTable = new DataTable('#results', {
|
||||
"order": [6, 'desc'],
|
||||
"order": [[6, 'desc']],
|
||||
"columns": [
|
||||
{ "data": "name" }, // Name
|
||||
{ "data": "category" }, // Category
|
||||
{ "data": "region" }, // Region
|
||||
{ "data": "type" }, // Type
|
||||
{ "data": "size" }, // Size
|
||||
{ "data": "date" }, // Date
|
||||
{ "data": "score" }, // Search Score
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
{ "data": "play", "orderable": false } // Play button column
|
||||
<% } %>
|
||||
],
|
||||
"lengthMenu": [100, { label: 'All', value: -1 }, 50, 25, 10],
|
||||
"paging": false,
|
||||
"filter": false,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<!-- CSS (load bootstrap from a CDN) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
<style>
|
||||
|
||||
html, body {
|
||||
|
||||
Reference in New Issue
Block a user