mirror of
https://github.com/linuxserver/emulatorjs.git
synced 2026-02-23 00:01:26 +08:00
Compare commits
187 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4573321563 | ||
|
|
bd7f6d3f88 | ||
|
|
82b3a6a6a0 | ||
|
|
557f22224e | ||
|
|
b387fb9cc3 | ||
|
|
b623e88332 | ||
|
|
f0ab089cf0 | ||
|
|
6f8a9ce8c2 | ||
|
|
e4bcad3fc0 | ||
|
|
57064e9845 | ||
|
|
9f9f47d1c8 | ||
|
|
e65c91993d | ||
|
|
3f36deb276 | ||
|
|
ea4519329b | ||
|
|
7cdb6e3b57 | ||
|
|
c08f7ab515 | ||
|
|
79988afbbd | ||
|
|
1d788281cc | ||
|
|
90a56c94c4 | ||
|
|
a079add1ec | ||
|
|
c2a780c49c | ||
|
|
a6458e550f | ||
|
|
fbcb869418 | ||
|
|
600b15a14e | ||
|
|
37d2b56166 | ||
|
|
d9f12b9a1a | ||
|
|
27324af975 | ||
|
|
b9797373ad | ||
|
|
651f8ce155 | ||
|
|
a678343d43 | ||
|
|
b0f7a2f7b6 | ||
|
|
0eb869fed4 | ||
|
|
1d9b8664bc | ||
|
|
449d16c987 | ||
|
|
6584c8d4dc | ||
|
|
a2b9cf106b | ||
|
|
e140c2cc10 | ||
|
|
fae1f9197c | ||
|
|
c2806a7c78 | ||
|
|
874a46120d | ||
|
|
8feda628be | ||
|
|
80d64355cd | ||
|
|
fae49c691d | ||
|
|
873003df4a | ||
|
|
e83be0a9ba | ||
|
|
2f297564d0 | ||
|
|
f5fa453950 | ||
|
|
28e33df774 | ||
|
|
e75b6287e5 | ||
|
|
cbe1082d0a | ||
|
|
50f212fa1a | ||
|
|
8c72479d3b | ||
|
|
17ff9e79fd | ||
|
|
0696142487 | ||
|
|
a4ce532fd1 | ||
|
|
44007f3116 | ||
|
|
94b18116db | ||
|
|
1aa74deed8 | ||
|
|
c13e408385 | ||
|
|
23fa41ec70 | ||
|
|
308cd82d39 | ||
|
|
342db57497 | ||
|
|
b9321545b1 | ||
|
|
3e17aa9294 | ||
|
|
d631f7c4a8 | ||
|
|
e6d3fa72c8 | ||
|
|
cbfc6c6f23 | ||
|
|
1f226be887 | ||
|
|
042099abc1 | ||
|
|
776d87357f | ||
|
|
dbedcf9e81 | ||
|
|
a24fbf7ad7 | ||
|
|
bf4db4f0f7 | ||
|
|
520982b81d | ||
|
|
ce6111cc29 | ||
|
|
c8017c636b | ||
|
|
b86b5651bf | ||
|
|
6d120c03ef | ||
|
|
6c7054bdd6 | ||
|
|
3dce99913e | ||
|
|
380673528a | ||
|
|
e60ef4e1a1 | ||
|
|
e2800587be | ||
|
|
e43884d0f8 | ||
|
|
f49adf73cf | ||
|
|
58b36683ec | ||
|
|
14e20915e6 | ||
|
|
83074b098f | ||
|
|
29299dd4f3 | ||
|
|
c3b709d754 | ||
|
|
d6c959c837 | ||
|
|
647a539b48 | ||
|
|
0a3b8b995a | ||
|
|
6f18af3ad9 | ||
|
|
eb12616e25 | ||
|
|
c1e2100977 | ||
|
|
2f70c0f39c | ||
|
|
8aefcd43b4 | ||
|
|
4305f2c785 | ||
|
|
bf1688dea7 | ||
|
|
0d7304309a | ||
|
|
c1a5e9aac8 | ||
|
|
2439a5be1b | ||
|
|
a65e573397 | ||
|
|
1343be1f2a | ||
|
|
e04062ebb2 | ||
|
|
74d995f89a | ||
|
|
f32c1c5dda | ||
|
|
36b72c458d | ||
|
|
8ad353ef39 | ||
|
|
03c2bbb3f1 | ||
|
|
d41e36804d | ||
|
|
026c2e0fec | ||
|
|
d4e8ddac67 | ||
|
|
7c0ca14354 | ||
|
|
6d6cb79e51 | ||
|
|
0ed562a23e | ||
|
|
2662a64d74 | ||
|
|
5a35fc72aa | ||
|
|
ccb348266e | ||
|
|
8762578119 | ||
|
|
8a90212bd5 | ||
|
|
62fa7aacfb | ||
|
|
4fd9130923 | ||
|
|
86cbc01522 | ||
|
|
e4142a174e | ||
|
|
fa4ed179c4 | ||
|
|
d4b89e8918 | ||
|
|
e0f64eece4 | ||
|
|
7b64c351b7 | ||
|
|
0de3699ca6 | ||
|
|
22972007fc | ||
|
|
f4d24d2af0 | ||
|
|
14ad375a59 | ||
|
|
091315f216 | ||
|
|
d0bf858a86 | ||
|
|
e5e478c3ea | ||
|
|
2285047e4e | ||
|
|
e923fb4b36 | ||
|
|
f769d5b4ca | ||
|
|
3bfd9bc79f | ||
|
|
98c7320fb0 | ||
|
|
20dde69386 | ||
|
|
1fd899411b | ||
|
|
fc746b5241 | ||
|
|
ea608bc3e7 | ||
|
|
3563002a5e | ||
|
|
f92ca43ff6 | ||
|
|
5ba579779e | ||
|
|
d83c1df092 | ||
|
|
5a74df2c1a | ||
|
|
7adab856b1 | ||
|
|
f7270fe1e2 | ||
|
|
f91e765945 | ||
|
|
d8187aede1 | ||
|
|
6846dca619 | ||
|
|
3c49f952ee | ||
|
|
a2056142f3 | ||
|
|
11877e810f | ||
|
|
abad3c60b1 | ||
|
|
286246f35f | ||
|
|
9ca44bfa3a | ||
|
|
d33d2bf2fd | ||
|
|
87f1dee870 | ||
|
|
2f8e5daa02 | ||
|
|
090df82d0b | ||
|
|
65c95d815d | ||
|
|
2ac417c52e | ||
|
|
2086a2483c | ||
|
|
b5c6e0ff52 | ||
|
|
a402615e69 | ||
|
|
5689aa158f | ||
|
|
9faa6ad723 | ||
|
|
9f2e5972dd | ||
|
|
e19eace32f | ||
|
|
eb34a9466d | ||
|
|
e41c5f6bdd | ||
|
|
44326348b4 | ||
|
|
0965bbc11d | ||
|
|
6592d1846b | ||
|
|
568d235c1b | ||
|
|
8ba4a7d40d | ||
|
|
ef6c838fb6 | ||
|
|
ec235acf0e | ||
|
|
168553701c | ||
|
|
219a9e0c93 | ||
|
|
c824f31225 |
16
.github/workflows/call_issue_pr_tracker.yml
vendored
Normal file
16
.github/workflows/call_issue_pr_tracker.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Issue & PR Tracker
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened,reopened,labeled,unlabeled,closed]
|
||||
pull_request_target:
|
||||
types: [opened,reopened,review_requested,review_request_removed,labeled,unlabeled,closed]
|
||||
pull_request_review:
|
||||
types: [submitted,edited,dismissed]
|
||||
|
||||
jobs:
|
||||
manage-project:
|
||||
permissions:
|
||||
issues: write
|
||||
uses: linuxserver/github-workflows/.github/workflows/issue-pr-tracker.yml@v1
|
||||
secrets: inherit
|
||||
13
.github/workflows/call_issues_cron.yml
vendored
Normal file
13
.github/workflows/call_issues_cron.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Mark stale issues and pull requests
|
||||
on:
|
||||
schedule:
|
||||
- cron: '35 15 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
uses: linuxserver/github-workflows/.github/workflows/issues-cron.yml@v1
|
||||
secrets: inherit
|
||||
10
README.md
10
README.md
@ -1,14 +1,12 @@
|
||||
# EmulatorJS helper
|
||||
|
||||
The purpose of this application is to provide a self hosted solution for people looking to run Retro games in their web browsers. It consists of a backend application for scanning roms and ingesting art assets with a static frontend application for serving those files via any basic webserver. The backend appplication is more or less a run once deal, when you have finalized the menus how you like them the resulting static files of the frontend no longer require the backend helper.
|
||||
The purpose of this application is to provide a self hosted solution for people looking to run Retro games in their web browsers. It consists of a backend application for scanning roms and ingesting art assets with a static frontend application for serving those files via any basic webserver. The backend application is more or less a run once deal, when you have finalized the menus how you like them the resulting static files of the frontend no longer require the backend helper.
|
||||
|
||||
This idea was born from a single need, I wanted to run retro games on my Xbox which now includes a modern chromium based web browser with Microsoft Edge. Web based emulators are popular online but always ingesting roms from external sources is a pain not to mention prone to being taken down at any time, also their interfaces are never designed around basic controller input making navigation difficult with something like an Xbox controller.
|
||||
|
||||
**It is important to note that the current emulator used for this frontend is obfuscated code, efforts are being made to [reverese engineer it](https://github.com/ethanaobrien/emulatorjs/) but you should know it can potentially reach out to third party services if you manually enable features like netplay (this should never happen in a stock setup). We are in the process to transitioning to libretro cores for emulators, currently 27/30 emulators have been replaced.**
|
||||
|
||||
## For Users
|
||||
|
||||
We reccomend using the docker container located [here](https://github.com/linuxserver/docker-emulatorjs). This will spin up:
|
||||
We recommend using the docker container located [here](https://github.com/linuxserver/docker-emulatorjs). This will spin up:
|
||||
|
||||
* The nodejs backend used for managing your configuration files and rom art assets.
|
||||
* An NGINX webserver to serve the static frontend files.
|
||||
@ -29,7 +27,7 @@ All images should be a png and run through pngquant for web optimization IE:
|
||||
pngquant yourimage.png
|
||||
```
|
||||
|
||||
Videos should be mp4 format and compressed pretty heavily to be optimized for web while load quickly in the web interface, IE using ffmpeg: (aac is also acceptible, but I had better results with mp3)
|
||||
Videos should be mp4 format and compressed pretty heavily to be optimized for web while load quickly in the web interface, IE using ffmpeg: (aac is also acceptable, but I had better results with mp3)
|
||||
|
||||
```
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 32 -acodec libmp3lame -qscale:a 7 output.mp4
|
||||
@ -147,4 +145,4 @@ The application can be accesed at:
|
||||
* Backend - http://localhost:3000
|
||||
* Frontend - http://localhost:3000/frontend/index.html
|
||||
|
||||
The application is written in jqueary and scanning is backed by a simple bash helper. The idea is to keep it as simple as possible, people will not be spending hours in the backend interface. The frontend is all designed around converting json configs, images, videos, and roms into a useable web based emulator.
|
||||
The application is written in jQuery and scanning is backed by a simple bash helper. The idea is to keep it as simple as possible, people will not be spending hours in the backend interface. The frontend is all designed around converting json configs, images, videos, and roms into a useable web based emulator.
|
||||
|
||||
70
frontend/css/filebrowser.css
Normal file
70
frontend/css/filebrowser.css
Normal file
@ -0,0 +1,70 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.directory, .file {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 2px solid #000;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
margin: 0px !important;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fileTable {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: 2px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
tr:nth-child(even){
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
tr:hover, button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
#dropzone {
|
||||
position: fixed; top: 0; left: 0;
|
||||
z-index: 9999999999;
|
||||
width: 100%; height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
transition: visibility 175ms, opacity 175ms;
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid rgba(0,0,0,.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: black;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
-webkit-animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@-webkit-keyframes spin {
|
||||
to { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
3
frontend/css/folder.svg
Normal file
3
frontend/css/folder.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg enable-background="new 0 0 60 60" version="1.1" viewBox="0 0 60 60" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m59.188 41.363-1.444-0.277c-0.677-0.13-1.21-0.573-1.462-1.216-0.253-0.642-0.163-1.329 0.245-1.885l0.907-1.236c0.292-0.398 0.25-0.95-0.1-1.299l-1.959-1.958c-0.339-0.339-0.872-0.389-1.268-0.121l-1.217 0.824c-0.331 0.224-0.71 0.343-1.099 0.343-0.844 0-1.787-0.576-1.956-1.676l-0.233-1.515c-0.075-0.487-0.495-0.848-0.988-0.848h-2.77c-0.479 0-0.892 0.341-0.982 0.812l-0.358 1.865c-0.201 1.051-1.13 1.601-1.942 1.601-0.389 0-0.769-0.118-1.099-0.342l-1.572-1.064c-0.397-0.268-0.929-0.218-1.268 0.121l-1.959 1.958c-0.35 0.349-0.392 0.9-0.1 1.299l0.907 1.236c0.408 0.556 0.498 1.243 0.245 1.885-0.252 0.643-0.785 1.086-1.462 1.216l-1.444 0.277c-0.469 0.091-0.81 0.503-0.81 0.983v2.77c0 0.493 0.36 0.913 0.848 0.988l1.515 0.233c0.683 0.105 1.232 0.528 1.508 1.16 0.276 0.633 0.212 1.323-0.175 1.895l-0.824 1.217c-0.269 0.396-0.218 0.929 0.121 1.268l1.958 1.959c0.348 0.349 0.899 0.391 1.299 0.1l1.236-0.907c0.342-0.252 0.742-0.385 1.156-0.385 0.813 0 1.742 0.551 1.944 1.602l0.277 1.444c0.091 0.471 0.503 0.812 0.982 0.812h2.77c0.493 0 0.913-0.36 0.988-0.848l0.164-1.065c0.169-1.1 1.113-1.676 1.958-1.676 0.413 0 0.813 0.134 1.156 0.386l0.869 0.638c0.4 0.292 0.951 0.25 1.299-0.1l1.958-1.959c0.339-0.339 0.39-0.871 0.121-1.268l-0.824-1.217c-0.387-0.571-0.451-1.262-0.175-1.895 0.275-0.632 0.825-1.055 1.508-1.16l1.515-0.233c0.488-0.077 0.848-0.497 0.848-0.99v-2.77c0-0.479-0.341-0.891-0.812-0.982zm-1.188 2.895-0.667 0.103c-1.354 0.208-2.489 1.082-3.036 2.337-0.548 1.256-0.416 2.682 0.352 3.815l0.361 0.533-0.768 0.768-0.177-0.13c-0.689-0.505-1.498-0.772-2.34-0.772-1.979 0-3.635 1.418-3.935 3.371l-0.032 0.217h-1.086l-0.121-0.633c-0.359-1.868-2.004-3.225-3.909-3.225-0.843 0-1.651 0.268-2.34 0.771l-0.544 0.399-0.768-0.768 0.361-0.533c0.768-1.134 0.899-2.56 0.352-3.815s-1.683-2.129-3.036-2.337l-0.667-0.101v-1.086l0.633-0.121c1.346-0.259 2.447-1.175 2.947-2.449 0.5-1.275 0.315-2.695-0.494-3.8l-0.399-0.544 0.768-0.768 0.889 0.602c0.662 0.448 1.43 0.686 2.22 0.686 1.905 0 3.548-1.355 3.907-3.224l0.202-1.054h1.086l0.103 0.667c0.301 1.953 1.955 3.371 3.934 3.371 0.789 0 1.556-0.237 2.219-0.687l0.533-0.361 0.768 0.768-0.399 0.544c-0.81 1.104-0.994 2.524-0.494 3.8 0.5 1.274 1.602 2.19 2.947 2.449l0.63 0.121v1.086z"/><path d="m47 38.5c-2.757 0-5 2.243-5 5s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5zm0 8c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3z"/><path d="m26.515 10.5-5-7h-21.515v53h37.372l-4.253-4.256c-0.339-0.339-0.39-0.871-0.121-1.268l1.303-1.923-2.453-0.377c-0.488-0.074-0.848-0.494-0.848-0.988v-7.821c0-0.479 0.341-0.892 0.812-0.982l2.28-0.438-1.469-2c-0.292-0.398-0.25-0.95 0.1-1.299l5.532-5.529c0.34-0.339 0.87-0.389 1.268-0.121l2.338 1.583 0.054-0.315 0.471-2.454c0.091-0.471 0.503-0.812 0.982-0.812h7.821c0.493 0 0.913 0.36 0.988 0.848l0.378 2.453 1.922-1.303c0.396-0.268 0.929-0.218 1.268 0.121l4.255 4.253v-23.372h-33.485zm31.485 7h-26.485l-3.571-5h30.056v5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
1
frontend/css/fullscreen.svg
Normal file
1
frontend/css/fullscreen.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg enable-background="new 0 0 512 512" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M0,0v512h512V0H0z M477.867,477.867H34.133V34.133h443.733V477.867z"/><polygon points="126.53 102.4 199.11 102.4 199.11 68.267 68.267 68.267 68.267 199.11 102.4 199.11 102.4 126.54 198.42 222.56 222.56 198.42"/><polygon points="222.56 313.58 198.42 289.44 102.4 385.47 102.4 312.89 68.267 312.89 68.267 443.73 199.11 443.73 199.11 409.6 126.54 409.6"/><polygon points="409.6 312.89 409.6 385.47 313.58 289.44 289.44 313.58 385.46 409.6 312.89 409.6 312.89 443.73 443.73 443.73 443.73 312.89"/><polygon points="312.89 68.267 312.89 102.4 385.47 102.4 289.44 198.42 313.58 222.56 409.6 126.54 409.6 199.11 443.73 199.11 443.73 68.267"/></svg>
|
||||
|
After Width: | Height: | Size: 820 B |
@ -69,15 +69,40 @@ canvas {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.full-button {
|
||||
#startEmu {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 5vw;
|
||||
border: 3px solid;
|
||||
border-radius: 1vw;
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.file-button {
|
||||
position: absolute;
|
||||
top: 1vh;
|
||||
left: 1vw;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-size: 3vw;
|
||||
width: 1.7vw;
|
||||
z-index: 999999;
|
||||
filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(82deg) brightness(105%) contrast(105%);
|
||||
}
|
||||
|
||||
.full-main {
|
||||
position: absolute;
|
||||
top: 1vh;
|
||||
left: 4vw;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
width: 1.7vw;
|
||||
z-index: 999999;
|
||||
filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(82deg) brightness(105%) contrast(105%);
|
||||
}
|
||||
|
||||
@keyframes growonce {
|
||||
|
||||
59
frontend/filebrowser.html
Normal file
59
frontend/filebrowser.html
Normal file
@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="css/filebrowser.css">
|
||||
<link rel="stylesheet" href="css/vendor/jquery.modal.min.css">
|
||||
<script type="text/javascript" src="js/vendor/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="js/vendor/jquery.modal.min.js"></script>
|
||||
<script type="text/javascript" src="js/vendor/browserfs.min.js"></script>
|
||||
<script type="text/javascript" src="js/vendor/jszip.min.js"></script>
|
||||
<script type="text/javascript" src="js/filebrowser.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="popup" class="modal">
|
||||
<div id="popupContents"></div>
|
||||
</div>
|
||||
<div class="hidden" id="profile">
|
||||
<span id=profileButtons">
|
||||
<span id="username"></span>
|
||||
<span id="loginEntry">
|
||||
<input id="user" type="text" placeholder="user"></input>
|
||||
<input id="pass" type="password" placeholder="pass"></input>
|
||||
<button id="login" onclick="login()">Login</button>
|
||||
</span>
|
||||
<button id="logout" class="hidden" onclick="logout()">Logout</button>
|
||||
<span id="syncButtons" class="hidden">
|
||||
<button class="right" onclick="pushProfile()">↑ Push to Server</button>
|
||||
<button class="right" onclick="pullProfile()">↓ Pull from Server</button>
|
||||
</span>
|
||||
<button id="defaultPull" class="right" onclick="defaultProfile()">Pull Default Config</button>
|
||||
</span>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="hidden" id="touchpad">
|
||||
<p>TouchScreen control type: <select name="touch" id="touch">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="false">Disabled</option>
|
||||
<option value="default">Default</option>
|
||||
<option value="simple">Simple</option>
|
||||
<option value="modern">Modern</option>
|
||||
</select>
|
||||
<button onclick="touchSave()">Save</button>
|
||||
</p>
|
||||
<hr>
|
||||
</div>
|
||||
<p>File browser:</p>
|
||||
<span id="buttons">
|
||||
<input id="folderName" type="text" placeholder="Enter Directory Name"></input>
|
||||
<button onclick="createFolder()">Create Folder</button>
|
||||
<button onclick="$('#uploadInput').trigger( 'click' )">Upload Files</button>
|
||||
<input class="hidden" id="uploadInput" type='file' onchange="upload(this);" multiple>
|
||||
<button class="right" onclick="downloadBackup()">DL Full Backup</button>
|
||||
<button class="right" onclick="$('#uploadBackup').trigger( 'click' )">Upload Full Backup</button>
|
||||
<input class="hidden" id="uploadBackup" type='file' onchange="uploadBackup(this);" />
|
||||
</span>
|
||||
<div id="filebrowser"></div>
|
||||
<div ondrop="dropFiles(event)" ondragover="allowDrop(event)" style="visibility:hidden;opacity:0" id="dropzone">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/icon.png
Normal file
BIN
frontend/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -1,15 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="apple-touch-icon" href="icon.png" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||
<link rel="stylesheet" href="css/index.css">
|
||||
<script src="js/vendor/jquery.min.js"></script>
|
||||
<script src="js/vendor/jquery.hotkeys.js"></script>
|
||||
<script src="js/vendor/browserfs.min.js"></script>
|
||||
<script src="js/vendor/hammer.min.js"></script>
|
||||
<script src="js/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="menu">
|
||||
<a target="_blank" href="filebrowser.html">
|
||||
<img class="file-button" src="css/folder.svg"/>
|
||||
</a>
|
||||
<img class="full-main" onclick="fullscreen()" src="css/fullscreen.svg"/>
|
||||
<video autoplay muted loop id="bgvid">
|
||||
<source id="vid" type="video/mp4">
|
||||
</video>
|
||||
|
||||
585
frontend/js/filebrowser.js
Normal file
585
frontend/js/filebrowser.js
Normal file
@ -0,0 +1,585 @@
|
||||
//Default vars
|
||||
var endPoint = 'profile'
|
||||
var storeName = 'RetroArch';
|
||||
var afs;
|
||||
BrowserFS.install(window);
|
||||
var fs = require('fs');
|
||||
var mfs = new BrowserFS.FileSystem.MountableFileSystem();
|
||||
var postSettings = {method:'POST',headers:{Accept:'application/json','Content-Type':'application/json'}};
|
||||
|
||||
// Render file list
|
||||
async function renderFiles(directory) {
|
||||
directory = directory.replace("//","/");
|
||||
directory = directory.replace("|","'");
|
||||
let directoryClean = directory.replace("'","|");
|
||||
if ((directory !== '/') && (directory.endsWith('/'))) {
|
||||
directory = directory.slice(0, -1);
|
||||
}
|
||||
$('#filebrowser').empty();
|
||||
$('#filebrowser').data('directory', directory);
|
||||
$('#filebrowser').append($('<div>').text(directory));
|
||||
let items = await fs.readdirSync(directory);
|
||||
let baseName = directory.split('/').slice(-1)[0];
|
||||
let parentFolder = directory.replace(baseName,'');
|
||||
let parentLink = $('<td>').addClass('directory').attr('onclick', 'renderFiles(\'' + parentFolder + '\');').text('..');
|
||||
if (directoryClean == '/') {
|
||||
directoryClean = '';
|
||||
}
|
||||
let table = $('<table>').addClass('fileTable');
|
||||
let tableHeader = $('<tr>');
|
||||
for await (name of ['Name', 'Type', 'Delete (NO WARNING)']) {
|
||||
tableHeader.append($('<th>').text(name));
|
||||
}
|
||||
let parentRow = $('<tr>');
|
||||
for await (item of [parentLink, $('<td>').text('Parent'), $('<td>')]) {
|
||||
parentRow.append(item);
|
||||
}
|
||||
table.append(tableHeader,parentRow);
|
||||
$('#filebrowser').append(table);
|
||||
items.sort();
|
||||
if (items.length > 0) {
|
||||
let dirs = [];
|
||||
let files = [];
|
||||
for await (let item of items) {
|
||||
if (fs.lstatSync(directory + '/' + item).isDirectory()) {
|
||||
dirs.push(item)
|
||||
} else {
|
||||
files.push(item)
|
||||
}
|
||||
}
|
||||
if (dirs.length > 0) {
|
||||
for await (let dir of dirs) {
|
||||
let tableRow = $('<tr>');
|
||||
let dirClean = dir.replace("'","|");
|
||||
let link = $('<td>').addClass('directory').attr('onclick', 'renderFiles(\'' + directoryClean + '/' + dirClean + '\');').text(dir);
|
||||
let type = $('<td>').text('Dir');
|
||||
let del = $('<td>').append($('<button>').addClass('deleteButton').attr('onclick', 'deleter(\'' + directoryClean + '/' + dirClean + '\');').text('Delete'));
|
||||
for await (item of [link, type, del]) {
|
||||
tableRow.append(item);
|
||||
}
|
||||
table.append(tableRow);
|
||||
}
|
||||
}
|
||||
if (files.length > 0) {
|
||||
for await (let file of files) {
|
||||
let tableRow = $('<tr>');
|
||||
let fileClean = file.replace("'","|");
|
||||
let link = $('<td>').addClass('file').attr('onclick', 'downloadFile(\'' + directoryClean + '/' + fileClean + '\');').text(file);
|
||||
let type = $('<td>').text('File');
|
||||
let del = $('<td>').append($('<button>').addClass('deleteButton').attr('onclick', 'deleter(\'' + directoryClean + '/' + fileClean + '\');').text('Delete'));
|
||||
for await (item of [link, type, del]) {
|
||||
tableRow.append(item);
|
||||
}
|
||||
table.append(tableRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Download file when clicked
|
||||
async function downloadFile(file) {
|
||||
file = file.replace("|","'");
|
||||
let fileName = file.split('/').slice(-1)[0];
|
||||
let data = fs.readFileSync(file);
|
||||
let blob = new Blob([data], { type: "application/octetstream" });
|
||||
let url = window.URL || window.webkitURL;
|
||||
link = url.createObjectURL(blob);
|
||||
let a = $("<a />");
|
||||
a.attr("download", fileName);
|
||||
a.attr("href", link);
|
||||
$("body").append(a);
|
||||
a[0].click();
|
||||
$("body").remove(a);
|
||||
}
|
||||
|
||||
// Upload file to current directory
|
||||
async function upload(input) {
|
||||
let directory = $('#filebrowser').data('directory');
|
||||
if (directory == '/') {
|
||||
directoryUp = '';
|
||||
} else {
|
||||
directoryUp = directory;
|
||||
}
|
||||
if (input.files && input.files[0]) {
|
||||
for await (let file of input.files) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
let fileName = file.name;
|
||||
let data = e.target.result;
|
||||
fs.writeFileSync(directoryUp + '/' + fileName, Buffer.from(data));
|
||||
if (file == input.files[input.files.length - 1]) {
|
||||
renderFiles(directory);
|
||||
}
|
||||
}
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a directory
|
||||
async function createFolder() {
|
||||
let folderName = $('#folderName').val();
|
||||
$('#folderName').val('');
|
||||
if ((folderName.length == 0) || (folderName.includes('/'))) {
|
||||
alert('Bad or Null Directory Name');
|
||||
return '';
|
||||
}
|
||||
let directory = $('#filebrowser').data('directory');
|
||||
if (directory == '/') {
|
||||
directoryUp = '';
|
||||
} else {
|
||||
directoryUp = directory;
|
||||
}
|
||||
let createD = directoryUp + '/' + folderName;
|
||||
if (!fs.existsSync(createD)){
|
||||
fs.mkdirSync(createD);
|
||||
}
|
||||
renderFiles(directory);
|
||||
}
|
||||
|
||||
// Handle drag and drop
|
||||
async function dropFiles(ev) {
|
||||
ev.preventDefault();
|
||||
let directory = $('#filebrowser').data('directory');
|
||||
if (directory == '/') {
|
||||
directoryUp = '';
|
||||
} else {
|
||||
directoryUp = directory;
|
||||
}
|
||||
let items = await getAllFileEntries(event.dataTransfer.items);
|
||||
for await (let item of items) {
|
||||
let fullPath = item.fullPath;
|
||||
let dirPath = fullPath.split('/');
|
||||
// Check if directories need to be created
|
||||
if (dirPath.length > 2) {
|
||||
var startDir = directoryUp;
|
||||
dirPath.splice(dirPath.length - 1);
|
||||
dirPath.shift();
|
||||
for await (let dir of dirPath) {
|
||||
startDir = startDir + '/' + dir;
|
||||
if (!fs.existsSync(startDir)){
|
||||
fs.mkdirSync(startDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Write file
|
||||
item.file(async function(file) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
let data = e.target.result;
|
||||
fs.writeFileSync(directoryUp + '/' + fullPath, Buffer.from(data));
|
||||
if (item == items[items.length - 1]) {
|
||||
$('#dropzone').css({'visibility':'hidden','opacity':0});
|
||||
renderFiles(directory);
|
||||
}
|
||||
}
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Drop handler function to get all files
|
||||
async function getAllFileEntries(dataTransferItemList) {
|
||||
let fileEntries = [];
|
||||
// Use BFS to traverse entire directory/file structure
|
||||
let queue = [];
|
||||
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
|
||||
for (let i = 0; i < dataTransferItemList.length; i++) {
|
||||
queue.push(dataTransferItemList[i].webkitGetAsEntry());
|
||||
}
|
||||
while (queue.length > 0) {
|
||||
let entry = queue.shift();
|
||||
if (entry.isFile) {
|
||||
fileEntries.push(entry);
|
||||
} else if (entry.isDirectory) {
|
||||
let reader = entry.createReader();
|
||||
queue.push(...await readAllDirectoryEntries(reader));
|
||||
}
|
||||
}
|
||||
return fileEntries;
|
||||
}
|
||||
// Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array
|
||||
async function readAllDirectoryEntries(directoryReader) {
|
||||
let entries = [];
|
||||
let readEntries = await readEntriesPromise(directoryReader);
|
||||
while (readEntries.length > 0) {
|
||||
entries.push(...readEntries);
|
||||
readEntries = await readEntriesPromise(directoryReader);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
// Wrap readEntries in a promise to make working with readEntries easier
|
||||
async function readEntriesPromise(directoryReader) {
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
directoryReader.readEntries(resolve, reject);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
var lastTarget;
|
||||
// Change style when hover files
|
||||
window.addEventListener('dragenter', function(ev) {
|
||||
lastTarget = ev.target;
|
||||
$('#dropzone').css({'visibility':'','opacity':1});
|
||||
});
|
||||
|
||||
// Change style when leave hover files
|
||||
window.addEventListener("dragleave", function(ev) {
|
||||
if(ev.target == lastTarget || ev.target == document) {
|
||||
$('#dropzone').css({'visibility':'hidden','opacity':0});
|
||||
}
|
||||
});
|
||||
|
||||
// Disabled default drag and drop
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
// Delete file or folder
|
||||
async function deleter(item) {
|
||||
let directory = $('#filebrowser').data('directory');
|
||||
item = item.replace("|","'");
|
||||
if (fs.lstatSync(item).isDirectory()) {
|
||||
await rmDir(item);
|
||||
} else {
|
||||
fs.unlinkSync(item);
|
||||
}
|
||||
renderFiles(directory);
|
||||
}
|
||||
|
||||
// Download a full backup of all files
|
||||
async function downloadBackup() {
|
||||
let zip = new JSZip();
|
||||
let items = await fs.readdirSync('/');
|
||||
async function addToZip(item) {
|
||||
if (fs.lstatSync(item).isDirectory()) {
|
||||
let items = await fs.readdirSync(item);
|
||||
if (items.length > 0) {
|
||||
for await (let subPath of items) {
|
||||
await addToZip(item + '/' + subPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let data = fs.readFileSync(item);
|
||||
let zipPath = item.replace(/^\//,'');
|
||||
zip.file(zipPath, data);
|
||||
}
|
||||
return ''
|
||||
}
|
||||
for await (let item of items) {
|
||||
await addToZip(item);
|
||||
}
|
||||
zip.generateAsync({type:"blob"}).then(function callback(blob) {
|
||||
let url = window.URL || window.webkitURL;
|
||||
link = url.createObjectURL(blob);
|
||||
let a = $("<a />");
|
||||
a.attr("download", storeName + '.zip');
|
||||
a.attr("href", link);
|
||||
$("body").append(a);
|
||||
a[0].click();
|
||||
$("body").remove(a);
|
||||
});
|
||||
}
|
||||
|
||||
// Full delete directory
|
||||
async function rmDir(dirPath) {
|
||||
try {
|
||||
let files = fs.readdirSync(dirPath);
|
||||
if (files.length > 0) {
|
||||
for await (let file of files) {
|
||||
var filePath = dirPath + '/' + file;
|
||||
if (fs.statSync(filePath).isFile()) {
|
||||
fs.unlinkSync(filePath);
|
||||
} else {
|
||||
await rmDir(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dirPath !== '/') {
|
||||
fs.rmdirSync(dirPath);
|
||||
}
|
||||
if (fs.readdirSync(dirPath).length !== 0) {
|
||||
rmDir(dirPath);
|
||||
return '';
|
||||
}
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
// Upload a full backup
|
||||
async function uploadBackup(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
$('#filebrowser').empty();
|
||||
$('#filebrowser').append($('<div>').attr('id','loading'));
|
||||
let reader = new FileReader();
|
||||
reader.onload = async function(e) {
|
||||
let data = e.target.result;
|
||||
let zip = new JSZip();
|
||||
// Load zip from data
|
||||
zip.loadAsync(data).then(async function(contents) {
|
||||
// Purge current storage
|
||||
await rmDir('/');
|
||||
// Unzip the files to the FS by name
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (fileName.endsWith('/')) {
|
||||
if (! fs.existsSync('/' + fileName)) {
|
||||
fs.mkdirSync('/' + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (! fileName.endsWith('/')) {
|
||||
zip.file(fileName).async('arraybuffer').then(function(content) {
|
||||
fs.writeFileSync('/' + fileName, Buffer.from(content));
|
||||
});
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
reader.readAsArrayBuffer(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Render profile
|
||||
async function loadProfile() {
|
||||
let res = await fetch(endPoint);
|
||||
let ping = await res.text();
|
||||
if (ping == 'pong') {
|
||||
$('#profile').removeClass('hidden');
|
||||
}
|
||||
if ((localStorage.getItem('user')) && (localStorage.getItem('pass'))) {
|
||||
loggedIn();
|
||||
}
|
||||
}
|
||||
|
||||
// Login
|
||||
async function login() {
|
||||
try {
|
||||
let user = $('#user').val();
|
||||
let pass = $('#pass').val();
|
||||
$('#user').val('');
|
||||
$('#pass').val('');
|
||||
let loginSettings = postSettings;
|
||||
loginSettings.body = JSON.stringify({user:user,pass:pass,type:'login'});
|
||||
let res = await fetch(endPoint,loginSettings);
|
||||
let json = await res.json();
|
||||
if (json.status == 'success') {
|
||||
localStorage.setItem('user',json.user);
|
||||
localStorage.setItem('pass',pass);
|
||||
loggedIn();
|
||||
} else {
|
||||
alert('Bad login');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
function logout() {
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('pass');
|
||||
loggedOut();
|
||||
}
|
||||
|
||||
// Render as logged in
|
||||
function loggedIn() {
|
||||
let user = localStorage.getItem('user');
|
||||
$('#loginEntry').addClass('hidden');
|
||||
$('#defaultPull').addClass('hidden');
|
||||
$('#logout').removeClass('hidden');
|
||||
$('#syncButtons').removeClass('hidden');
|
||||
$('#username').text(user);
|
||||
}
|
||||
|
||||
// Render as logged out
|
||||
function loggedOut() {
|
||||
$('#username').empty();
|
||||
$('#syncButtons').addClass('hidden');
|
||||
$('#logout').addClass('hidden');
|
||||
$('#defaultPull').removeClass('hidden');
|
||||
$('#loginEntry').removeClass('hidden');
|
||||
}
|
||||
|
||||
// Pull profile from server
|
||||
async function pullProfile() {
|
||||
$('#filebrowser').empty();
|
||||
$('#filebrowser').append($('<div>').attr('id','loading'));
|
||||
let user = localStorage.getItem('user');
|
||||
let pass = localStorage.getItem('pass');
|
||||
let loginSettings = postSettings;
|
||||
loginSettings.body = JSON.stringify({user:user,pass:pass,type:'pull'});
|
||||
let res = await fetch(endPoint,loginSettings);
|
||||
let json = await res.json();
|
||||
if (json.status == 'success') {
|
||||
let zip = new JSZip();
|
||||
// Purge current storage
|
||||
await rmDir('/');
|
||||
let baseData = json.data;
|
||||
// Load zip from data
|
||||
zip.loadAsync(baseData, {base64: true}).then(async function(contents) {
|
||||
// Unzip the files to the FS by name
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (fileName.endsWith('/')) {
|
||||
if (! fs.existsSync('/' + fileName)) {
|
||||
fs.mkdirSync('/' + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (! fileName.endsWith('/')) {
|
||||
zip.file(fileName).async('arraybuffer').then(function(content) {
|
||||
fs.writeFileSync('/' + fileName, Buffer.from(content));
|
||||
});
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
alert('Error pulling profile');
|
||||
}
|
||||
}
|
||||
|
||||
async function defaultProfile() {
|
||||
$('#filebrowser').empty();
|
||||
$('#filebrowser').append($('<div>').attr('id','loading'));
|
||||
let loginSettings = postSettings;
|
||||
loginSettings.body = JSON.stringify({type:'default'});
|
||||
let res = await fetch(endPoint,loginSettings);
|
||||
let json = await res.json();
|
||||
if (json.status == 'success') {
|
||||
let zip = new JSZip();
|
||||
// Purge current storage
|
||||
await rmDir('/');
|
||||
let baseData = json.data;
|
||||
// Load zip from data
|
||||
zip.loadAsync(baseData, {base64: true}).then(async function(contents) {
|
||||
// Unzip the files to the FS by name
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (fileName.endsWith('/')) {
|
||||
if (! fs.existsSync('/' + fileName)) {
|
||||
fs.mkdirSync('/' + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (! fileName.endsWith('/')) {
|
||||
zip.file(fileName).async('arraybuffer').then(function(content) {
|
||||
fs.writeFileSync('/' + fileName, Buffer.from(content));
|
||||
});
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
alert('Error pulling profile');
|
||||
}
|
||||
}
|
||||
|
||||
// Push profile to server
|
||||
async function pushProfile() {
|
||||
$('#filebrowser').empty();
|
||||
$('#filebrowser').append($('<div>').attr('id','loading'));
|
||||
let zip = new JSZip();
|
||||
let items = await fs.readdirSync('/');
|
||||
async function addToZip(item) {
|
||||
if (fs.lstatSync(item).isDirectory()) {
|
||||
let items = await fs.readdirSync(item);
|
||||
if (items.length > 0) {
|
||||
for await (let subPath of items) {
|
||||
await addToZip(item + '/' + subPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let data = fs.readFileSync(item);
|
||||
let zipPath = item.replace(/^\//,'');
|
||||
zip.file(zipPath, data);
|
||||
}
|
||||
return ''
|
||||
}
|
||||
for await (let item of items) {
|
||||
await addToZip(item);
|
||||
}
|
||||
zip.generateAsync({type:"base64"}).then(async function callback(base64) {
|
||||
try {
|
||||
let user = localStorage.getItem('user');
|
||||
let pass = localStorage.getItem('pass');
|
||||
let loginSettings = postSettings;
|
||||
loginSettings.body = JSON.stringify({user:user,pass:pass,type:'push',data:base64});
|
||||
let res = await fetch(endPoint,loginSettings);
|
||||
let json = await res.json();
|
||||
if (json.status == 'success') {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error uploading profile');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error uploading profile');
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load and show touch input options on touch devices
|
||||
function loadTouchInput() {
|
||||
if (window.orientation !== undefined) {
|
||||
$('#touchpad').removeClass('hidden');
|
||||
if (localStorage.getItem('touchpad') !== null) {
|
||||
if (localStorage.getItem('touchpad') == 'false') {
|
||||
$('#touch').val('false');
|
||||
} else if (localStorage.getItem('touchpad') == 'default') {
|
||||
$('#touch').val('default');
|
||||
} else if (localStorage.getItem('touchpad') == 'simple') {
|
||||
$('#touch').val('simple');
|
||||
} else if (localStorage.getItem('touchpad') == 'modern') {
|
||||
$('#touch').val('modern');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Save touch input option
|
||||
function touchSave() {
|
||||
let option = $('#touch').val();
|
||||
if (option == 'auto') {
|
||||
localStorage.removeItem('touchpad');
|
||||
} else {
|
||||
localStorage.setItem('touchpad',option);
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Create Async filestore
|
||||
async function setupFileSystem() {
|
||||
var imfs = new BrowserFS.FileSystem.InMemory();
|
||||
afs = new BrowserFS.FileSystem.AsyncMirror(imfs,
|
||||
new BrowserFS.FileSystem.IndexedDB(async function(e, fs) {
|
||||
afs.initialize(async function(e) {
|
||||
console.log('IndexedDB setup successful');
|
||||
setupMounts();
|
||||
});
|
||||
},
|
||||
storeName));
|
||||
};
|
||||
|
||||
// Setup mounts
|
||||
async function setupMounts() {
|
||||
mfs.mount('/', afs);
|
||||
BrowserFS.initialize(mfs);
|
||||
renderFiles('/');
|
||||
}
|
||||
|
||||
// On page load
|
||||
window.onload = function() {
|
||||
setupFileSystem();
|
||||
loadProfile();
|
||||
loadTouchInput();
|
||||
}
|
||||
@ -13,6 +13,12 @@ var defaultKeys = [
|
||||
'has_video',
|
||||
'multi_disc'
|
||||
];
|
||||
localStorage.setItem('retroArch',true);
|
||||
var gamePadType;
|
||||
var isSafari = navigator.vendor && navigator.vendor.indexOf('Apple') > -1 &&
|
||||
navigator.userAgent &&
|
||||
navigator.userAgent.indexOf('CriOS') == -1 &&
|
||||
navigator.userAgent.indexOf('FxiOS') == -1;
|
||||
|
||||
//// Helper functions ////
|
||||
// Debounce calls to functions that run heavy
|
||||
@ -125,7 +131,7 @@ function launch(active_item) {
|
||||
config.items['Disc ' + count.toString()].has_video = false;
|
||||
config.items['Disc ' + count.toString()].multi_disc = 0;
|
||||
};
|
||||
rendermenu(config, 0);
|
||||
rendermenu([config, 0]);
|
||||
} else if (type == 'game') {
|
||||
// Disable keyevents and hash watching
|
||||
window.exit = false;
|
||||
@ -138,13 +144,24 @@ function launch(active_item) {
|
||||
if (emulator.startsWith('libretro-')) {
|
||||
var emulator = emulator.replace('libretro-','');
|
||||
var script = 'js/libretro.js'
|
||||
var EJSemu = false;
|
||||
EJS_onGameStart = function() {
|
||||
Module.requestFullscreen(false);
|
||||
gameStarted = true;
|
||||
let gps = navigator.getGamepads();
|
||||
if (gps) {
|
||||
for (let gp of gps) {
|
||||
if (gp) {
|
||||
let gpEvt = new GamepadEvent("gamepadconnected",{gamepad: gp});
|
||||
window.dispatchEvent(gpEvt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var script = 'data/loader.js'
|
||||
var EJSemu = true;
|
||||
EJS_onGameStart = function() {
|
||||
gameStarted = true;
|
||||
document.querySelectorAll('[data-btn="fullscreen"]')[0].click();
|
||||
}
|
||||
};
|
||||
@ -162,9 +179,33 @@ function launch(active_item) {
|
||||
EJS_biosUrl = bios;
|
||||
}
|
||||
EJS_player = '#game';
|
||||
EJS_gameUrl = rom_path + name + rom_extension;
|
||||
EJS_gameUrl = encodeURI(rom_path + name + rom_extension);
|
||||
EJS_core = emulator;
|
||||
EJS_pathtodata = 'data/';
|
||||
// Load touch screen interface
|
||||
if ((! EJSemu) && (window.orientation !== undefined) && localStorage.getItem('touchpad') !== 'false' && !navigator.getGamepads()?.[0]) {
|
||||
// Determine type to render
|
||||
if (localStorage.getItem('touchpad') !== null) {
|
||||
if (localStorage.getItem('touchpad') == 'simple') {
|
||||
gamePadType = 'simple';
|
||||
} else if (localStorage.getItem('touchpad') == 'modern') {
|
||||
gamePadType = 'modern';
|
||||
}
|
||||
} else {
|
||||
if ((emulator == 'gearboy') || (emulator == 'fceumm') || (emulator == 'mednafen_vb') || (emulator == 'gambatte') || (emulator == 'stella2014') || (emulator == 'prosystem') || (emulator == 'mednafen_pce_fast')) {
|
||||
gamePadType = 'simple';
|
||||
} else if ((emulator == 'prboom') || (emulator == 'mednafen_psx') || (emulator == 'tyrquake') || (emulator == 'melonds') || (emulator == 'melonds_threaded')) {
|
||||
gamePadType = 'modern';
|
||||
} else if (emulator == 'mupen64plus_next') {
|
||||
gamePadType = 'n64';
|
||||
}
|
||||
}
|
||||
var touchDiv = $('<div>').attr('id','gamepad');
|
||||
$('body').append(touchDiv);
|
||||
var touchscript = document.createElement('script');
|
||||
touchscript.src = 'js/touchpad.js';
|
||||
document.head.append(touchscript);
|
||||
}
|
||||
// Load in EJS loader
|
||||
var loaderscript = document.createElement('script');
|
||||
loaderscript.src = script;
|
||||
@ -201,7 +242,9 @@ function launch(active_item) {
|
||||
}
|
||||
|
||||
//// Page rendering logic ////
|
||||
async function rendermenu(data, active_item) {
|
||||
async function rendermenu(datas) {
|
||||
var data = datas[0];
|
||||
var active_item = datas[1];
|
||||
// Set default variables
|
||||
var portrait = window.orientation;
|
||||
$('#menu').data('config', data);
|
||||
@ -209,6 +252,10 @@ async function rendermenu(data, active_item) {
|
||||
$('#menu').data('root', root);
|
||||
var parent = data.parent;
|
||||
var items = data.items;
|
||||
if (Object.keys(items).length == 0) {
|
||||
alert('No items to load, please add some games');
|
||||
return '';
|
||||
};
|
||||
for await (var name of Object.keys(items)) {
|
||||
var item = data.items[name];
|
||||
if ((item.hasOwnProperty('cloneof')) && (items.hasOwnProperty(item.cloneof))) {
|
||||
@ -244,7 +291,22 @@ async function rendermenu(data, active_item) {
|
||||
};
|
||||
// Loop items loaded from json to build the game menu divs
|
||||
var count = 0;
|
||||
var jumpIndex = {};
|
||||
var letters = "abcdefghijklmnopqrstuvwxyz".split("");
|
||||
for await (var name of Object.keys(items)) {
|
||||
// Generate an index table based on alphabetical order ignoring numbers
|
||||
if (count == 0) {
|
||||
jumpIndex['0'] = count;
|
||||
} else {
|
||||
letterLoop:
|
||||
for (let letter of letters) {
|
||||
let startLetter = name.charAt(0).toLowerCase();
|
||||
if ((! jumpIndex.hasOwnProperty(startLetter)) && (letter == startLetter)) {
|
||||
jumpIndex[letter] = count;
|
||||
break letterLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
var item = data.items[name];
|
||||
// Use text or image tag based on logo
|
||||
if (item.hasOwnProperty('has_logo')) {
|
||||
@ -259,7 +321,7 @@ async function rendermenu(data, active_item) {
|
||||
var romName = name;
|
||||
};
|
||||
if (has_logo == true) {
|
||||
logo_html = '<img class="menu-img">';
|
||||
logo_html = '<img class="menu-img" alt="'+ romName +'" title="'+ romName +'">';
|
||||
} else {
|
||||
logo_html = '<p class="menu-img">' + name + '</p>';
|
||||
};
|
||||
@ -282,7 +344,7 @@ async function rendermenu(data, active_item) {
|
||||
</div>\
|
||||
</div>');
|
||||
count++
|
||||
};
|
||||
};
|
||||
// Render active list
|
||||
for await (var active_num of [...Array(display_items).keys()]) {
|
||||
$('#active-list').append('<div id="active' + active_num + '" class="menu-div"></div>');
|
||||
@ -302,10 +364,16 @@ async function rendermenu(data, active_item) {
|
||||
$('.menu-img').css({'max-height': image_height});
|
||||
highlight(active_item);
|
||||
// Move items up
|
||||
function moveUp() {
|
||||
$('#bgvid').prop('muted', false);
|
||||
$('#bgvid').prop('volume', 0.5);
|
||||
active_item--
|
||||
function moveUp(num) {
|
||||
if (! isSafari) {
|
||||
$('#bgvid').prop('muted', false);
|
||||
$('#bgvid').prop('volume', 0.5);
|
||||
}
|
||||
if (typeof num == 'number') {
|
||||
active_item = (active_item - num);
|
||||
} else {
|
||||
active_item--
|
||||
}
|
||||
if (active_item < 0) {
|
||||
active_item = items_length;
|
||||
}
|
||||
@ -319,10 +387,16 @@ async function rendermenu(data, active_item) {
|
||||
highlight(active_item);
|
||||
};
|
||||
// Move items down
|
||||
function moveDown() {
|
||||
$('#bgvid').prop('muted', false);
|
||||
$('#bgvid').prop('volume', 0.5);
|
||||
active_item++
|
||||
function moveDown(num) {
|
||||
if (! isSafari) {
|
||||
$('#bgvid').prop('muted', false);
|
||||
$('#bgvid').prop('volume', 0.5);
|
||||
}
|
||||
if (typeof num == 'number') {
|
||||
active_item = (active_item + num);
|
||||
} else {
|
||||
active_item++
|
||||
}
|
||||
if (active_item > items_length) {
|
||||
active_item = 0;
|
||||
}
|
||||
@ -335,34 +409,83 @@ async function rendermenu(data, active_item) {
|
||||
}
|
||||
highlight(active_item);
|
||||
};
|
||||
// Jump items up
|
||||
async function indexUp() {
|
||||
for await (index of Object.keys(jumpIndex).reverse()) {
|
||||
if (active_item > jumpIndex[index]) {
|
||||
let jumpNum = (active_item - jumpIndex[index]);
|
||||
moveUp(jumpNum);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Jump items down
|
||||
async function indexDown() {
|
||||
for await (index of Object.keys(jumpIndex)) {
|
||||
if (jumpIndex[index] > active_item) {
|
||||
let jumpNum = (jumpIndex[index] - active_item);
|
||||
moveDown(jumpNum);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Capture key events for menu navigation
|
||||
let upPressed = false;
|
||||
let downPressed = false;
|
||||
$(document).keydown(function(event) {
|
||||
// Scroll on keypress
|
||||
if (event.key == 'ArrowDown') {
|
||||
downPressed = true;
|
||||
moveDown();
|
||||
}
|
||||
if (event.key == 'ArrowUp') {
|
||||
upPressed = true;
|
||||
moveUp();
|
||||
}
|
||||
// Scroll faster with diagnols
|
||||
if ((event.key == 'ArrowRight') && ((upPressed == false) && (downPressed == true))) {
|
||||
moveDown(10);
|
||||
}
|
||||
if ((event.key == 'ArrowRight') && ((upPressed == true) && (downPressed == false))) {
|
||||
moveUp(10);
|
||||
}
|
||||
// Load item
|
||||
if ((event.key == 'ArrowRight') || (event.key == 'Enter')) {
|
||||
if (((event.key == 'ArrowRight') || (event.key == 'Enter')) && ((upPressed == false) && (downPressed == false))) {
|
||||
$('#i' + active_item).click();
|
||||
}
|
||||
// Go to Parent
|
||||
if (event.key == 'ArrowLeft') {
|
||||
window.location.href = '#' + parent;
|
||||
}
|
||||
// Jump Down
|
||||
if (event.key == 'PageDown') {
|
||||
indexDown();
|
||||
}
|
||||
// Jump Up
|
||||
if (event.key == 'PageUp') {
|
||||
indexUp();
|
||||
}
|
||||
});
|
||||
// Remove events for multi key presses
|
||||
$(document).keyup(function(event) {
|
||||
if (event.key == 'ArrowDown') {
|
||||
downPressed = false;
|
||||
}
|
||||
if (event.key == 'ArrowUp') {
|
||||
upPressed = false;
|
||||
}
|
||||
});
|
||||
//// Touch controls ////
|
||||
// Scroll up wheel
|
||||
// Scroll wheel
|
||||
async function scroll(ev) {
|
||||
window.scrollKill = false;
|
||||
var scrolling = setInterval(() => {
|
||||
if (window.scrollKill == false) {
|
||||
if (ev.additionalEvent == 'panup') {
|
||||
moveDown();
|
||||
} else if (ev.additionalEvent == 'pandown') {
|
||||
moveUp();
|
||||
} else {
|
||||
moveDown(5);
|
||||
} else if (ev.additionalEvent == 'pandown') {
|
||||
moveUp(5);
|
||||
} else {
|
||||
clearInterval(scrolling);
|
||||
};
|
||||
} else {
|
||||
@ -376,7 +499,7 @@ async function rendermenu(data, active_item) {
|
||||
};
|
||||
var mc = new Hammer(document.getElementById('menu'));
|
||||
mc.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
|
||||
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 250 });
|
||||
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 180 });
|
||||
mc.on("swipeup", moveDown);
|
||||
mc.on("swipedown", moveUp);
|
||||
mc.on("panstart", scroll);
|
||||
@ -403,16 +526,168 @@ async function rendermenu(data, active_item) {
|
||||
};
|
||||
return false;
|
||||
});
|
||||
};
|
||||
//// GamePad controls ////
|
||||
let scrollDelay
|
||||
let animReq
|
||||
let homeTimer;
|
||||
let home = 0;
|
||||
let homePressed = false;
|
||||
let gpUpdate;
|
||||
function gameLoop() {
|
||||
// Handle if buttons are missing
|
||||
function buttonsMissing(axes,buttons) {
|
||||
var missing = true;
|
||||
axes.forEach(function(i) {
|
||||
if (typeof gp.axes[i] === 'undefined') {
|
||||
missing = false;
|
||||
}
|
||||
});
|
||||
buttons.forEach(function(i) {
|
||||
if (typeof gp.buttons[i] === 'undefined') {
|
||||
missing = false;
|
||||
}
|
||||
});
|
||||
return missing;
|
||||
}
|
||||
let gamePads = navigator.getGamepads();
|
||||
if (!gamePads?.[0]) return;
|
||||
let gp = gamePads[0];
|
||||
if (window.location.hash != "#game") {
|
||||
gameStarted = false;
|
||||
if (!scrollDelay) {
|
||||
// Analog down
|
||||
if ((buttonsMissing([1,3],[])) && (gp.axes[1] > .5 || gp.axes[3] > .5)) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 200);
|
||||
moveDown();
|
||||
// Analog up
|
||||
} else if ((buttonsMissing([1,3],[])) && (gp.axes[1] < -.5 || gp.axes[3] < -.5)) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 200);
|
||||
moveUp();
|
||||
// D-pad down
|
||||
} else if ((buttonsMissing([],[13])) && (gp.buttons[13].pressed)) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 200);
|
||||
moveDown();
|
||||
// D-pad up
|
||||
} else if ((buttonsMissing([],[12])) && (gp.buttons[12].pressed)) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 200);
|
||||
moveUp();
|
||||
// R1 index down
|
||||
} else if ((buttonsMissing([],[5])) && (gp.buttons[5].pressed)) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 200);
|
||||
indexDown();
|
||||
// L1 index up
|
||||
} else if ((buttonsMissing([],[4])) && (gp.buttons[4].pressed)) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 200);
|
||||
indexUp();
|
||||
// Analog L2 scroll up by strength
|
||||
} else if ((buttonsMissing([],[6])) && (gp.buttons[6].pressed)) {
|
||||
if (gp.buttons[6].value) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, (Math.abs(gp.buttons[6].value - 1) * 200) + 40);
|
||||
} else {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 40);
|
||||
}
|
||||
moveUp();
|
||||
// Analog R2 scroll down by strength
|
||||
} else if ((buttonsMissing([],[7])) && (gp.buttons[7].pressed)) {
|
||||
if (gp.buttons[7].value) {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, (Math.abs(gp.buttons[7].value - 1) * 200) + 40);
|
||||
} else {
|
||||
scrollDelay = setTimeout(() => scrollDelay = undefined, 40);
|
||||
}
|
||||
moveDown();
|
||||
}
|
||||
}
|
||||
if (gp.timestamp == gpUpdate) {
|
||||
animReq = requestAnimationFrame(gameLoop);
|
||||
return;
|
||||
}
|
||||
gpUpdate = gp.timestamp
|
||||
if (gp.buttons[0].pressed) {
|
||||
if ($('#i' + active_item.toString()).data('type') == "game") {
|
||||
cancelAnimationFrame(animReq);
|
||||
}
|
||||
$('#i' + active_item).click();
|
||||
return;
|
||||
} else if (gp.buttons[1].pressed && parent && '#' + parent != window.location.hash) {
|
||||
window.location.href = '#' + parent;
|
||||
return;
|
||||
} else if ((buttonsMissing([],[16])) && (gp.buttons[16].pressed && window.location.hash != '#main')) {
|
||||
window.location.href = '#main';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (gp.timestamp == gpUpdate) {
|
||||
animReq = requestAnimationFrame(gameLoop);
|
||||
return;
|
||||
}
|
||||
gpUpdate = gp.timestamp
|
||||
try {
|
||||
if (!gameStarted && gp.buttons[1].pressed && parent) {
|
||||
window.location.href = '#' + parent;
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
// Press home button 3 times to exit game
|
||||
if ((buttonsMissing([],[16])) && (!gp.buttons[16].pressed && homePressed)) {
|
||||
home++;
|
||||
homePressed = false;
|
||||
}
|
||||
if ((buttonsMissing([],[16])) && (gp.buttons[16].pressed)) {
|
||||
clearTimeout(homeTimer)
|
||||
homeTimer = setTimeout(() => home = 0, 500)
|
||||
homePressed = true
|
||||
}
|
||||
if ((buttonsMissing([],[16])) && (gp.buttons[16].pressed && home >= 2)) {
|
||||
window.location.href = '#' + parent;
|
||||
}
|
||||
}
|
||||
animReq = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
window.addEventListener("gamepadconnected", gameLoop)
|
||||
window.addEventListener("gamepaddisconnected", cancelAnimationFrame(animReq))
|
||||
window.addEventListener("load", () => {
|
||||
var gameStarted = false;
|
||||
let gps = navigator.getGamepads();
|
||||
if (gps) {
|
||||
for (let gp of gps) {
|
||||
let gpEvt = new GamepadEvent("gamepadconnected", {
|
||||
gamepad: gp
|
||||
})
|
||||
window.dispatchEvent(gpEvt)
|
||||
}
|
||||
}
|
||||
});
|
||||
window.addEventListener("hashchange", gameLoop);
|
||||
}
|
||||
|
||||
// Go fullscreen
|
||||
function fullscreen() {
|
||||
let page = document.documentElement;
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
if (page.requestFullscreen) {
|
||||
page.requestFullscreen();
|
||||
} else if (page.webkitRequestFullscreen) {
|
||||
page.webkitRequestFullscreen();
|
||||
} else if (page.msRequestFullscreen) {
|
||||
page.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the json profile selected
|
||||
function loadjson(name, active) {
|
||||
var url = 'user/config/' + name + '.json';
|
||||
fetch(url,Init)
|
||||
.then((resp) => resp.json())
|
||||
.then((data) => {
|
||||
rendermenu(data, active)
|
||||
});
|
||||
async function loadjson(name, active_item) {
|
||||
if (name == 'preview') {
|
||||
var url = 'user/hashes/preview.json';
|
||||
} else {
|
||||
var url = 'user/config/' + name + '.json';
|
||||
}
|
||||
let response = await fetch(url,Init);
|
||||
let data = await response.json();
|
||||
rendermenu([data, active_item]);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
@ -421,8 +696,8 @@ window.onload = function() {
|
||||
} else {
|
||||
var hash = window.location.hash.replace('#','');
|
||||
var name = hash.split('---')[0];
|
||||
var active = hash.split('---')[1];
|
||||
loadjson(name, active);
|
||||
let active_item = hash.split('---')[1];
|
||||
loadjson(name, active_item);
|
||||
}
|
||||
$(window).on('hashchange', function() {
|
||||
window.location.reload();
|
||||
|
||||
File diff suppressed because one or more lines are too long
704
frontend/js/touchpad.js
Normal file
704
frontend/js/touchpad.js
Normal file
@ -0,0 +1,704 @@
|
||||
(function(global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.screenGamepad = {}));
|
||||
}(this, (function(exports) {
|
||||
var extendStatics = function(d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({
|
||||
__proto__: []
|
||||
}
|
||||
instanceof Array && function(d, b) {
|
||||
d.__proto__ = b;
|
||||
}) ||
|
||||
function(d, b) {
|
||||
for (var p in b)
|
||||
if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p];
|
||||
};
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
|
||||
function __extends(d, b) {
|
||||
extendStatics(d, b);
|
||||
|
||||
function __() {
|
||||
this.constructor = d;
|
||||
}
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
}
|
||||
|
||||
var EventDispatcher = (function() {
|
||||
function EventDispatcher() {
|
||||
this._listeners = {};
|
||||
}
|
||||
EventDispatcher.prototype.addEventListener = function(type, listener) {
|
||||
var listeners = this._listeners;
|
||||
if (listeners[type] === undefined)
|
||||
listeners[type] = [];
|
||||
if (listeners[type].indexOf(listener) === -1)
|
||||
listeners[type].push(listener);
|
||||
};
|
||||
EventDispatcher.prototype.removeEventListener = function(type, listener) {
|
||||
var listeners = this._listeners;
|
||||
var listenerArray = listeners[type];
|
||||
if (listenerArray !== undefined) {
|
||||
var index = listenerArray.indexOf(listener);
|
||||
if (index !== -1)
|
||||
listenerArray.splice(index, 1);
|
||||
}
|
||||
};
|
||||
EventDispatcher.prototype.removeAllEventListeners = function(type) {
|
||||
if (!type) {
|
||||
this._listeners = {};
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(this._listeners[type]))
|
||||
this._listeners[type].length = 0;
|
||||
};
|
||||
EventDispatcher.prototype.dispatchEvent = function(event) {
|
||||
var listeners = this._listeners;
|
||||
var listenerArray = listeners[event.type];
|
||||
if (listenerArray !== undefined) {
|
||||
event.target = this;
|
||||
var array = listenerArray.slice(0);
|
||||
for (var i = 0, l = array.length; i < l; i++) {
|
||||
array[i].call(this, event);
|
||||
}
|
||||
}
|
||||
};
|
||||
return EventDispatcher;
|
||||
}());
|
||||
|
||||
function findTouchEventById(event, identifier) {
|
||||
for (var i = 0, l = event.changedTouches.length; i < l; i++) {
|
||||
if (identifier === event.changedTouches[i].identifier) {
|
||||
return event.changedTouches[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isTouchEvent(event) {
|
||||
return 'TouchEvent' in window && event instanceof TouchEvent;
|
||||
}
|
||||
|
||||
var $style = document.createElement('style');
|
||||
$style.innerHTML = `
|
||||
.screenGamepad-Joystick {
|
||||
cursor: pointer;
|
||||
-ms-touch-action : none;
|
||||
touch-action : none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
.screenGamepad-Joystick__Button {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 20%;
|
||||
box-sizing: border-box;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333;
|
||||
background: rgba( 255, 255, 255, .5 );
|
||||
z-index: 2147483647;
|
||||
}`;
|
||||
document.head.insertBefore($style, document.head.firstChild);
|
||||
var Joystick = (function(_super) {
|
||||
__extends(Joystick, _super);
|
||||
|
||||
function Joystick(options) {
|
||||
if (options === void 0) {
|
||||
options = {};
|
||||
}
|
||||
var _this = _super.call(this) || this;
|
||||
_this.domElement = document.createElement('div');
|
||||
_this._size = 128;
|
||||
_this._x = 0;
|
||||
_this._y = 0;
|
||||
_this._angle = 0;
|
||||
_this._isActive = false;
|
||||
_this._pointerId = -1;
|
||||
_this._elRect = new DOMRect();
|
||||
_this._$button = document.createElement('div');
|
||||
if (options.size)
|
||||
_this._size = options.size;
|
||||
_this.domElement.classList.add('screenGamepad-Joystick');
|
||||
_this.domElement.style.width = _this._size + "px";
|
||||
_this.domElement.style.height = _this._size + "px";
|
||||
_this._$button.classList.add('screenGamepad-Joystick__Button');
|
||||
_this.domElement.appendChild(_this._$button);
|
||||
var computePosition = function(offsetX, offsetY) {
|
||||
var x = offsetX / _this._size * 2 - 1;
|
||||
var y = -offsetY / _this._size * 2 + 1;
|
||||
if (x === 0 && y === 0) {
|
||||
_this._angle = 0;
|
||||
_this._x = 0;
|
||||
_this._y = 0;
|
||||
return;
|
||||
}
|
||||
_this._angle = Math.atan2(-y, -x) + Math.PI;
|
||||
var length = Math.min(Math.sqrt(x * x + y * y), 1);
|
||||
_this._x = Math.cos(_this._angle) * length;
|
||||
_this._y = Math.sin(_this._angle) * length;
|
||||
};
|
||||
var onButtonMove = function(event) {
|
||||
event.preventDefault();
|
||||
var _isTouchEvent = isTouchEvent(event);
|
||||
var _event = _isTouchEvent ?
|
||||
findTouchEventById(event, _this._pointerId) :
|
||||
event;
|
||||
if (!_event)
|
||||
return;
|
||||
var lastX = _this._x;
|
||||
var lastY = _this._y;
|
||||
var offsetX = (_event.clientX - window.pageXOffset - _this._elRect.left);
|
||||
var offsetY = (_event.clientY - window.pageYOffset - _this._elRect.top);
|
||||
computePosition(offsetX, offsetY);
|
||||
if (_this._x === lastX && _this._y === lastY)
|
||||
return;
|
||||
_this._update();
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
};
|
||||
var onButtonMoveEnd = function(event) {
|
||||
event.preventDefault();
|
||||
var _isTouchEvent = isTouchEvent(event);
|
||||
var _event = _isTouchEvent ?
|
||||
event.changedTouches[0] :
|
||||
event;
|
||||
if (_isTouchEvent && _event.identifier !== _this._pointerId)
|
||||
return;
|
||||
document.removeEventListener('mousemove', onButtonMove);
|
||||
document.removeEventListener('touchmove', onButtonMove, {
|
||||
passive: false
|
||||
});
|
||||
document.removeEventListener('mouseup', onButtonMoveEnd);
|
||||
document.removeEventListener('touchend', onButtonMoveEnd);
|
||||
_this._pointerId = -1;
|
||||
_this._isActive = false;
|
||||
_this._angle = 0;
|
||||
_this._x = 0;
|
||||
_this._y = 0;
|
||||
_this._update();
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
_this.dispatchEvent({
|
||||
type: 'inactive'
|
||||
});
|
||||
};
|
||||
var onButtonMoveStart = function(event) {
|
||||
event.preventDefault();
|
||||
var _isTouchEvent = isTouchEvent(event);
|
||||
var _event = _isTouchEvent ?
|
||||
event.changedTouches[0] :
|
||||
event;
|
||||
if (_isTouchEvent) {
|
||||
_this._pointerId = _event.identifier;
|
||||
}
|
||||
_this._elRect = _this.domElement.getBoundingClientRect();
|
||||
_this._isActive = true;
|
||||
var offsetX = (_event.clientX - window.pageXOffset - _this._elRect.left);
|
||||
var offsetY = (_event.clientY - window.pageYOffset - _this._elRect.top);
|
||||
computePosition(offsetX, offsetY);
|
||||
_this._update();
|
||||
document.addEventListener('mousemove', onButtonMove);
|
||||
document.addEventListener('touchmove', onButtonMove, {
|
||||
passive: false
|
||||
});
|
||||
document.addEventListener('mouseup', onButtonMoveEnd);
|
||||
document.addEventListener('touchend', onButtonMoveEnd);
|
||||
_this.dispatchEvent({
|
||||
type: 'active'
|
||||
});
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
};
|
||||
_this.domElement.addEventListener('mousedown', onButtonMoveStart);
|
||||
_this.domElement.addEventListener('touchstart', onButtonMoveStart);
|
||||
return _this;
|
||||
}
|
||||
Object.defineProperty(Joystick.prototype, "x", {
|
||||
get: function() {
|
||||
return this._x;
|
||||
},
|
||||
set: function(x) {
|
||||
this._x = x;
|
||||
this._update();
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Joystick.prototype, "y", {
|
||||
get: function() {
|
||||
return this._y;
|
||||
},
|
||||
set: function(y) {
|
||||
this._y = y;
|
||||
this._update();
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Joystick.prototype, "angle", {
|
||||
get: function() {
|
||||
return this._angle;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Joystick.prototype, "isActive", {
|
||||
get: function() {
|
||||
return this._isActive;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
Joystick.prototype._update = function() {
|
||||
this._$button.style.transition = this._isActive ? '' : 'transform .1s';
|
||||
if (this._x === 0 && this._y === 0) {
|
||||
this._$button.style.transform = "translate( 0px, 0px )";
|
||||
return;
|
||||
}
|
||||
var radius = this._size / 2;
|
||||
var x = this._x * radius;
|
||||
var y = -this._y * radius;
|
||||
this._$button.style.transform = "translate( " + x + "px, " + y + "px )";
|
||||
};
|
||||
return Joystick;
|
||||
}(EventDispatcher));
|
||||
|
||||
var SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
var $style$1 = document.createElement('style');
|
||||
$style$1.innerHTML = `
|
||||
.screenGamepad-Button {
|
||||
cursor: pointer;
|
||||
-ms-touch-action : none;
|
||||
touch-action : none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
.screenGamepad-Button__HitArea {
|
||||
color: rgba( 255, 255, 255, .5 );
|
||||
z-index: 2147483647;
|
||||
}`;
|
||||
document.head.insertBefore($style$1, document.head.firstChild);
|
||||
var Button = (function(_super) {
|
||||
__extends(Button, _super);
|
||||
|
||||
function Button(options) {
|
||||
if (options === void 0) {
|
||||
options = {};
|
||||
}
|
||||
var _this = _super.call(this) || this;
|
||||
_this.domElement = document.createElementNS(SVG_NS, 'svg');
|
||||
_this._size = 48;
|
||||
_this._isActive = false;
|
||||
_this._pointerId = -1;
|
||||
_this._$hitArea = document.createElementNS(SVG_NS, 'a');
|
||||
if (options.size)
|
||||
_this._size = options.size;
|
||||
_this._$hitArea.innerHTML = options.shape || Button.BUTTON_SHAPE_CIRCLE;
|
||||
_this.domElement.classList.add('screenGamepad-Button');
|
||||
_this.domElement.setAttribute('viewBox', '0 0 1 1');
|
||||
_this.domElement.style.width = _this._size + "px";
|
||||
_this.domElement.style.height = _this._size + "px";
|
||||
_this._$hitArea.classList.add('screenGamepad-Button__HitArea');
|
||||
_this.domElement.appendChild(_this._$hitArea);
|
||||
var hitRect = _this.domElement.createSVGRect();
|
||||
hitRect.width = 1;
|
||||
hitRect.height = 1;
|
||||
var onButtonMove = function(event) {
|
||||
event.preventDefault();
|
||||
var _isTouchEvent = isTouchEvent(event);
|
||||
var _event = _isTouchEvent ?
|
||||
findTouchEventById(event, _this._pointerId) :
|
||||
event;
|
||||
if (!_event)
|
||||
return;
|
||||
var x = _event.clientX;
|
||||
var y = _event.clientY;
|
||||
var $intersectedElement = document.elementFromPoint(x, y);
|
||||
var isIntersected = _this._$hitArea.contains($intersectedElement);
|
||||
if (isIntersected && !_this._isActive) {
|
||||
_this._isActive = true;
|
||||
_this._update();
|
||||
_this.dispatchEvent({
|
||||
type: 'active'
|
||||
});
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!isIntersected && _this._isActive) {
|
||||
_this._isActive = false;
|
||||
_this._update();
|
||||
_this.dispatchEvent({
|
||||
type: 'inactive'
|
||||
});
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
var onButtonUp = function(event) {
|
||||
event.preventDefault();
|
||||
document.removeEventListener('mousemove', onButtonMove);
|
||||
document.removeEventListener('touchmove', onButtonMove, {
|
||||
passive: false
|
||||
});
|
||||
document.removeEventListener('mouseup', onButtonUp);
|
||||
document.removeEventListener('touchend', onButtonUp);
|
||||
_this._pointerId = -1;
|
||||
if (!_this._isActive)
|
||||
return;
|
||||
_this._isActive = false;
|
||||
_this._update();
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
_this.dispatchEvent({
|
||||
type: 'inactive'
|
||||
});
|
||||
};
|
||||
var onButtonDown = function(event) {
|
||||
document.removeEventListener('mousemove', onButtonMove);
|
||||
document.removeEventListener('touchmove', onButtonMove, {
|
||||
passive: false
|
||||
});
|
||||
document.removeEventListener('mouseup', onButtonUp);
|
||||
document.removeEventListener('touchend', onButtonUp);
|
||||
event.preventDefault();
|
||||
var _isTouchEvent = isTouchEvent(event);
|
||||
if (_isTouchEvent) {
|
||||
var changedTouches = event.changedTouches;
|
||||
_this._pointerId = changedTouches[changedTouches.length - 1].identifier;
|
||||
}
|
||||
_this._isActive = true;
|
||||
_this._update();
|
||||
document.addEventListener('mousemove', onButtonMove);
|
||||
document.addEventListener('touchmove', onButtonMove, {
|
||||
passive: false
|
||||
});
|
||||
document.addEventListener('mouseup', onButtonUp);
|
||||
document.addEventListener('touchend', onButtonUp);
|
||||
_this.dispatchEvent({
|
||||
type: 'active'
|
||||
});
|
||||
_this.dispatchEvent({
|
||||
type: 'change'
|
||||
});
|
||||
};
|
||||
_this.domElement.addEventListener('mousedown', onButtonDown);
|
||||
_this.domElement.addEventListener('touchstart', onButtonDown);
|
||||
return _this;
|
||||
}
|
||||
Object.defineProperty(Button, "BUTTON_SHAPE_CIRCLE", {
|
||||
get: function() {
|
||||
return '<circle cx="0.5" cy="0.5" r="0.5" fill="currentColor" />';
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Button.prototype, "isActive", {
|
||||
get: function() {
|
||||
return this._isActive;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
Button.prototype._update = function() {};
|
||||
return Button;
|
||||
}(EventDispatcher));
|
||||
|
||||
var utils = {
|
||||
roundToStep: function(number, step) {
|
||||
return step * Math.round(number / step);
|
||||
},
|
||||
};
|
||||
|
||||
exports.Button = Button;
|
||||
exports.Joystick = Joystick;
|
||||
exports.utils = utils;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true
|
||||
});
|
||||
|
||||
})));
|
||||
|
||||
//// Fake touch controller ////
|
||||
var touchController = {
|
||||
axes: [0, 0, 0, 0],
|
||||
buttons: [],
|
||||
connected: false,
|
||||
id: "HTML5 Touch GamePad",
|
||||
index: 0,
|
||||
mapping: "standard",
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
for (let i of Array(18).keys()) {
|
||||
touchController.buttons.push({
|
||||
pressed: false,
|
||||
touched: false,
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
//// GamePads ////
|
||||
var directionStore;
|
||||
|
||||
//// Standard default D-pad snes style
|
||||
if (typeof gamePadType == 'undefined') {
|
||||
// Buttons
|
||||
var buttons = [
|
||||
[0, 'bottom', '40px', 'right', '80px', 'a'],
|
||||
[1, 'bottom', '70px', 'right', '30px', 'b'],
|
||||
[2, 'bottom', '90px', 'right', '110px', 'x'],
|
||||
[3, 'bottom', '120px', 'right', '60px', 'y'],
|
||||
[4, 'bottom', '210px', 'left', '30px', 'L1'],
|
||||
[5, 'bottom', '210px', 'right', '30px', 'R1'],
|
||||
[8, 'bottom', '50px', 'left', '42%', 'select'],
|
||||
[9, 'bottom', '50px', 'right', '42%', 'start']
|
||||
]
|
||||
// Joystick
|
||||
var joystick = new screenGamepad.Joystick();
|
||||
joystick.domElement.style.position = 'absolute';
|
||||
joystick.domElement.style.bottom = '40px';
|
||||
joystick.domElement.style.left = '20px';
|
||||
document.getElementById("gamepad").appendChild(joystick.domElement);
|
||||
joystick.addEventListener('change', function() {
|
||||
axesToDpad();
|
||||
});
|
||||
//// Basic controller type with no L+R and two buttons
|
||||
} else if (gamePadType == 'simple') {
|
||||
// Buttons
|
||||
var buttons = [
|
||||
[0, 'bottom', '40px', 'right', '80px', 'a'],
|
||||
[1, 'bottom', '70px', 'right', '30px', 'b'],
|
||||
[8, 'bottom', '50px', 'left', '42%', 'select'],
|
||||
[9, 'bottom', '50px', 'right', '42%', 'start']
|
||||
]
|
||||
// Joystick
|
||||
var joystick = new screenGamepad.Joystick();
|
||||
joystick.domElement.style.position = 'absolute';
|
||||
joystick.domElement.style.bottom = '40px';
|
||||
joystick.domElement.style.left = '20px';
|
||||
document.getElementById("gamepad").appendChild(joystick.domElement);
|
||||
joystick.addEventListener('change', function() {
|
||||
axesToDpad();
|
||||
});
|
||||
//// Dual Analog modern contoller
|
||||
} else if (gamePadType == 'modern') {
|
||||
// Buttons
|
||||
var buttons = [
|
||||
[0, 'bottom', '40px', 'right', '80px', 'a'],
|
||||
[1, 'bottom', '70px', 'right', '30px', 'b'],
|
||||
[2, 'bottom', '90px', 'right', '110px', 'x'],
|
||||
[3, 'bottom', '120px', 'right', '60px', 'y'],
|
||||
[4, 'bottom', '210px', 'left', '30px', 'L1'],
|
||||
[5, 'bottom', '210px', 'right', '30px', 'R1'],
|
||||
[6, 'bottom', '270px', 'left', '30px', 'L2'],
|
||||
[7, 'bottom', '270px', 'right', '30px', 'R2'],
|
||||
[8, 'bottom', '50px', 'left', '42%', 'select'],
|
||||
[9, 'bottom', '50px', 'right', '42%', 'start']
|
||||
]
|
||||
// Joystick
|
||||
var joystick = new screenGamepad.Joystick();
|
||||
joystick.domElement.style.position = 'absolute';
|
||||
joystick.domElement.style.bottom = '40px';
|
||||
joystick.domElement.style.left = '20px';
|
||||
document.getElementById("gamepad").appendChild(joystick.domElement);
|
||||
joystick.addEventListener('change', function() {
|
||||
axesToDpad();
|
||||
});
|
||||
// Analogs
|
||||
var left = new screenGamepad.Joystick();
|
||||
left.domElement.style.position = 'absolute';
|
||||
left.domElement.style.bottom = '40px';
|
||||
left.domElement.style.left = '200px';
|
||||
document.getElementById("gamepad").appendChild(left.domElement);
|
||||
left.addEventListener('change', function() {
|
||||
touchController.axes[0] = screenGamepad.utils.roundToStep(left.x, 0.1);
|
||||
touchController.axes[1] = screenGamepad.utils.roundToStep(left.x, 0.1);
|
||||
});
|
||||
var right = new screenGamepad.Joystick();
|
||||
right.domElement.style.position = 'absolute';
|
||||
right.domElement.style.bottom = '40px';
|
||||
right.domElement.style.right = '200px';
|
||||
document.getElementById("gamepad").appendChild(right.domElement);
|
||||
right.addEventListener('change', function() {
|
||||
touchController.axes[2] = screenGamepad.utils.roundToStep(right.x, 0.1);
|
||||
touchController.axes[3] = screenGamepad.utils.roundToStep(right.x, 0.1);
|
||||
});
|
||||
} else if (gamePadType == 'n64') {
|
||||
// Buttons
|
||||
var buttons = [
|
||||
[0, 'bottom', '40px', 'right', '110px', 'a'],
|
||||
[2, 'bottom', '90px', 'right', '140px', 'x'],
|
||||
[6, 'bottom', '60px', 'right', '200px', 'z'],
|
||||
[5, 'bottom', '230px', 'right', '30px', 'R1'],
|
||||
[4, 'bottom', '230px', 'left', '30px', 'L1'],
|
||||
[9, 'bottom', '50px', 'right', '50%', 'start']
|
||||
|
||||
]
|
||||
// Joystick
|
||||
var joystick = new screenGamepad.Joystick();
|
||||
joystick.domElement.style.position = 'absolute';
|
||||
joystick.domElement.style.bottom = '130px';
|
||||
joystick.domElement.style.left = '30px';
|
||||
document.getElementById("gamepad").appendChild(joystick.domElement);
|
||||
joystick.addEventListener('change', function() {
|
||||
axesToDpad();
|
||||
});
|
||||
// Analogs
|
||||
var left = new screenGamepad.Joystick();
|
||||
left.domElement.style.position = 'absolute';
|
||||
left.domElement.style.bottom = '30px';
|
||||
left.domElement.style.left = '30px';
|
||||
document.getElementById("gamepad").appendChild(left.domElement);
|
||||
left.addEventListener('change', function() {
|
||||
touchController.axes[0] = screenGamepad.utils.roundToStep(left.x, 0.1);
|
||||
touchController.axes[1] = screenGamepad.utils.roundToStep(left.x, 0.1);
|
||||
});
|
||||
// Default Retroarch uses the R analogue for the C buttons
|
||||
var right = new screenGamepad.Joystick();
|
||||
right.domElement.style.position = 'absolute';
|
||||
right.domElement.style.bottom = '120px';
|
||||
right.domElement.style.right = '30px';
|
||||
document.getElementById("gamepad").appendChild(right.domElement);
|
||||
right.addEventListener('change', function() {
|
||||
touchController.axes[2] = screenGamepad.utils.roundToStep(right.x, 0.1);
|
||||
touchController.axes[3] = screenGamepad.utils.roundToStep(right.x, 0.1);
|
||||
});
|
||||
}
|
||||
//// Render Buttons ////
|
||||
var vars = {};
|
||||
for (let button of buttons) {
|
||||
vars[button[0]] = new screenGamepad.Button();
|
||||
vars[button[0]].domElement.style.position = 'absolute';
|
||||
vars[button[0]].domElement.style[button[1]] = button[2];
|
||||
vars[button[0]].domElement.style[button[3]] = button[4];
|
||||
document.getElementById("gamepad").appendChild(vars[button[0]].domElement);
|
||||
vars[button[0]].addEventListener('change', function() {
|
||||
touchController.buttons[button[0]].pressed = vars[button[0]].isActive;
|
||||
connectGamePad();
|
||||
});
|
||||
}
|
||||
// Retroarch menu button
|
||||
if (localStorage.getItem('retroArch') == 'true') {
|
||||
menuButton = new screenGamepad.Button();
|
||||
menuButton.domElement.style.position = 'absolute';
|
||||
menuButton.domElement.style.top = '10px';
|
||||
menuButton.domElement.style.left = '10px';
|
||||
document.getElementById("gamepad").appendChild(menuButton.domElement);
|
||||
menuButton.addEventListener('change', function() {
|
||||
touchController.buttons[4].pressed = menuButton.isActive;
|
||||
touchController.buttons[5].pressed = menuButton.isActive;
|
||||
touchController.buttons[8].pressed = menuButton.isActive;
|
||||
touchController.buttons[9].pressed = menuButton.isActive;
|
||||
connectGamePad();
|
||||
});
|
||||
}
|
||||
//// Logic to convert an axes joystick to dpad
|
||||
function axesToDpad() {
|
||||
// 8 way direction with .24 deadzone
|
||||
let direction = axesToDirection({
|
||||
x: screenGamepad.utils.roundToStep(joystick.x, 0.1),
|
||||
y: screenGamepad.utils.roundToStep(joystick.y, 0.1)
|
||||
}, 0.24, 4);
|
||||
if (directionStore !== direction) {
|
||||
directionStore = direction;
|
||||
// Clear d-pad press
|
||||
touchController.buttons[12].pressed = false;
|
||||
touchController.buttons[13].pressed = false;
|
||||
touchController.buttons[14].pressed = false;
|
||||
touchController.buttons[15].pressed = false;
|
||||
// Press d-pad
|
||||
if (direction == 'up') {
|
||||
touchController.buttons[12].pressed = true;
|
||||
} else if (direction == 'down') {
|
||||
touchController.buttons[13].pressed = true;
|
||||
} else if (direction == 'left') {
|
||||
touchController.buttons[14].pressed = true;
|
||||
} else if (direction == 'right') {
|
||||
touchController.buttons[15].pressed = true;
|
||||
} else if (direction == 'upleft') {
|
||||
touchController.buttons[12].pressed = true;
|
||||
touchController.buttons[14].pressed = true;
|
||||
} else if (direction == 'upright') {
|
||||
touchController.buttons[12].pressed = true;
|
||||
touchController.buttons[15].pressed = true;
|
||||
} else if (direction == 'downright') {
|
||||
touchController.buttons[13].pressed = true;
|
||||
touchController.buttons[15].pressed = true;
|
||||
} else if (direction == 'downleft') {
|
||||
touchController.buttons[13].pressed = true;
|
||||
touchController.buttons[14].pressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
function axesToDirection(coord, deadzone, axes) {
|
||||
const angle = Math.atan2(coord.y, coord.x);
|
||||
const snapRadians = Math.PI / axes;
|
||||
const newAngle = snapRadians * Math.round(angle / snapRadians);
|
||||
let magnitude = Math.sqrt(coord.x * coord.x + coord.y * coord.y);
|
||||
if (magnitude <= deadzone) {
|
||||
return 'center';
|
||||
}
|
||||
if (magnitude > 1) {
|
||||
magnitude = 1;
|
||||
}
|
||||
let x = Math.cos(newAngle).toFixed(2);
|
||||
let y = Math.sin(newAngle).toFixed(2);
|
||||
if ((x == '0.00') && (y == '1.00')) {
|
||||
return 'up';
|
||||
} else if ((x == '0.00') && (y == '-1.00')) {
|
||||
return 'down';
|
||||
} else if (((x == '-1.00') && (y == '-0.00')) || ((x == '-1.00') && (y == '0.00'))) {
|
||||
return 'left';
|
||||
} else if ((x == '1.00') && (y == '0.00')) {
|
||||
return 'right';
|
||||
} else if ((x == '-0.71') && (y == '0.71')) {
|
||||
return 'upleft';
|
||||
} else if ((x == '0.71') && (y == '0.71')) {
|
||||
return 'upright';
|
||||
} else if ((x == '0.71') && (y == '-0.71')) {
|
||||
return 'downright';
|
||||
} else if ((x == '-0.71') && (y == '-0.71')) {
|
||||
return 'downleft';
|
||||
}
|
||||
}
|
||||
|
||||
//// Replace default contoller logic with ours ////
|
||||
navigator.getGamepads = function() {
|
||||
return {
|
||||
0: touchController,
|
||||
};
|
||||
};
|
||||
function connectGamePad() {
|
||||
let event = new Event("gamepadconnected");
|
||||
touchController.connected = true;
|
||||
touchController.timestamp = Math.floor(Date.now() / 1000);
|
||||
event.gamepad = touchController;
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
function disconnectGamePad() {
|
||||
let event = new Event("gamepaddisconnected");
|
||||
touchController.connected = false;
|
||||
touchController.timestamp = Math.floor(Date.now() / 1000);
|
||||
event.gamepad = touchController;
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
24
frontend/js/vendor/browserfs.min.js
vendored
24
frontend/js/vendor/browserfs.min.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/js/vendor/hammer.min.js
vendored
1
frontend/js/vendor/hammer.min.js
vendored
File diff suppressed because one or more lines are too long
28
frontend/js/vendor/ipfs-http-client.min.js
vendored
Normal file
28
frontend/js/vendor/ipfs-http-client.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
204
frontend/js/vendor/jquery.hotkeys.js
vendored
204
frontend/js/vendor/jquery.hotkeys.js
vendored
@ -1,204 +0,0 @@
|
||||
/*jslint browser: true*/
|
||||
/*jslint jquery: true*/
|
||||
|
||||
/*
|
||||
* jQuery Hotkeys Plugin
|
||||
* Copyright 2010, John Resig
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Based upon the plugin by Tzury Bar Yochay:
|
||||
* https://github.com/tzuryby/jquery.hotkeys
|
||||
*
|
||||
* Original idea by:
|
||||
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
|
||||
*/
|
||||
|
||||
/*
|
||||
* One small change is: now keys are passed by object { keys: '...' }
|
||||
* Might be useful, when you want to pass some other data to your handler
|
||||
*/
|
||||
|
||||
(function(jQuery) {
|
||||
|
||||
jQuery.hotkeys = {
|
||||
version: "0.2.0",
|
||||
|
||||
specialKeys: {
|
||||
8: "backspace",
|
||||
9: "tab",
|
||||
10: "return",
|
||||
13: "return",
|
||||
16: "shift",
|
||||
17: "ctrl",
|
||||
18: "alt",
|
||||
19: "pause",
|
||||
20: "capslock",
|
||||
27: "esc",
|
||||
32: "space",
|
||||
33: "pageup",
|
||||
34: "pagedown",
|
||||
35: "end",
|
||||
36: "home",
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
45: "insert",
|
||||
46: "del",
|
||||
59: ";",
|
||||
61: "=",
|
||||
96: "0",
|
||||
97: "1",
|
||||
98: "2",
|
||||
99: "3",
|
||||
100: "4",
|
||||
101: "5",
|
||||
102: "6",
|
||||
103: "7",
|
||||
104: "8",
|
||||
105: "9",
|
||||
106: "*",
|
||||
107: "+",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
112: "f1",
|
||||
113: "f2",
|
||||
114: "f3",
|
||||
115: "f4",
|
||||
116: "f5",
|
||||
117: "f6",
|
||||
118: "f7",
|
||||
119: "f8",
|
||||
120: "f9",
|
||||
121: "f10",
|
||||
122: "f11",
|
||||
123: "f12",
|
||||
144: "numlock",
|
||||
145: "scroll",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
},
|
||||
|
||||
shiftNums: {
|
||||
"`": "~",
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
";": ": ",
|
||||
"'": "\"",
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
"\\": "|"
|
||||
},
|
||||
|
||||
// excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
|
||||
textAcceptingInputTypes: [
|
||||
"text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
|
||||
"datetime-local", "search", "color", "tel"],
|
||||
|
||||
// default input types not to bind to unless bound directly
|
||||
textInputTypes: /textarea|input|select/i,
|
||||
|
||||
options: {
|
||||
filterInputAcceptingElements: true,
|
||||
filterTextInputs: true,
|
||||
filterContentEditable: true
|
||||
}
|
||||
};
|
||||
|
||||
function keyHandler(handleObj) {
|
||||
if (typeof handleObj.data === "string") {
|
||||
handleObj.data = {
|
||||
keys: handleObj.data
|
||||
};
|
||||
}
|
||||
|
||||
// Only care when a possible input has been specified
|
||||
if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var origHandler = handleObj.handler,
|
||||
keys = handleObj.data.keys.toLowerCase().split(" ");
|
||||
|
||||
handleObj.handler = function(event) {
|
||||
// Don't fire in text-accepting inputs that we didn't directly bind to
|
||||
if (this !== event.target &&
|
||||
(jQuery.hotkeys.options.filterInputAcceptingElements &&
|
||||
jQuery.hotkeys.textInputTypes.test(event.target.nodeName) ||
|
||||
(jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) ||
|
||||
(jQuery.hotkeys.options.filterTextInputs &&
|
||||
jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
|
||||
character = String.fromCharCode(event.which).toLowerCase(),
|
||||
modif = "",
|
||||
possible = {};
|
||||
|
||||
jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
|
||||
|
||||
if (event[specialKey + 'Key'] && special !== specialKey) {
|
||||
modif += specialKey + '+';
|
||||
}
|
||||
});
|
||||
|
||||
// metaKey is triggered off ctrlKey erronously
|
||||
if (event.metaKey && !event.ctrlKey && special !== "meta") {
|
||||
modif += "meta+";
|
||||
}
|
||||
|
||||
if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
|
||||
modif = modif.replace("alt+ctrl+shift+", "hyper+");
|
||||
}
|
||||
|
||||
if (special) {
|
||||
possible[modif + special] = true;
|
||||
}
|
||||
else {
|
||||
possible[modif + character] = true;
|
||||
possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
|
||||
|
||||
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
|
||||
if (modif === "shift+") {
|
||||
possible[jQuery.hotkeys.shiftNums[character]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0, l = keys.length; i < l; i++) {
|
||||
if (possible[keys[i]]) {
|
||||
return origHandler.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
jQuery.each(["keydown", "keyup", "keypress"], function() {
|
||||
jQuery.event.special[this] = {
|
||||
add: keyHandler
|
||||
};
|
||||
});
|
||||
|
||||
})(jQuery || this.jQuery || window.jQuery);
|
||||
13
frontend/js/vendor/jszip.min.js
vendored
Normal file
13
frontend/js/vendor/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
17
frontend/manifest.json
Normal file
17
frontend/manifest.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "EmulatorJS",
|
||||
"short_name": "EmulatorJS",
|
||||
"manifest_version": 2,
|
||||
"version": "1.9.2",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "500x500"
|
||||
}
|
||||
],
|
||||
"start_url": "/"
|
||||
}
|
||||
121
has_files.sh
121
has_files.sh
@ -1,28 +1,36 @@
|
||||
#! /bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
rom_path=$1
|
||||
rom_type=$2
|
||||
full_scan=$3
|
||||
# Set user folder
|
||||
if [ -d '/data' ]; then
|
||||
user_folder='/data'
|
||||
else
|
||||
user_folder='frontend/user'
|
||||
fi
|
||||
check_files=$(ls -1 "${user_folder}${rom_path}")
|
||||
check_files=$(find "${user_folder}${rom_path}" -maxdepth 1 -not -type d -and -not -name '.*' -exec basename {} \; | sort)
|
||||
|
||||
# Clear out old hashes
|
||||
if [ -d "${user_folder}/hashes/${rom_path}" ]; then
|
||||
rm -Rf "${user_folder}/hashes/${rom_path}"
|
||||
if [ "${full_scan}" = "true" ]; then
|
||||
if [ -d "${user_folder}/hashes/${rom_path}" ]; then
|
||||
rm -Rf "${user_folder}/hashes/${rom_path}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up for bad scans
|
||||
rm -Rf "${user_folder}/hashes/${rom_path}/tmp/"
|
||||
|
||||
# Process zip file hashes
|
||||
process_zip () {
|
||||
mkdir -p "${user_folder}/hashes/${rom_path}/tmp"
|
||||
echo "unzipping ${file}"
|
||||
unzip -q "${user_folder}${rom_path}/${file}" -d "${user_folder}/hashes/${rom_path}/tmp"
|
||||
unzip -j -q "${user_folder}${rom_path}/${file}" -d "${user_folder}/hashes/${rom_path}/tmp"
|
||||
rm "${user_folder}/hashes/${rom_path}/tmp/"*.{txt,nfo,xml,readme,README} &> /dev/null || :
|
||||
echo "hashing ${file}"
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/"* | awk '{print $1;exit}')
|
||||
firstfile=( "${user_folder}/hashes/${rom_path}/tmp/"* )
|
||||
sum=$(sha1sum "$firstfile" | awk '{print $1;exit}')
|
||||
rm -R "${user_folder}/hashes/${rom_path}/tmp/"
|
||||
printf ${sum^^} > "${user_folder}/hashes/${rom_path}/${file}.sha1"
|
||||
}
|
||||
@ -31,10 +39,12 @@ process_zip () {
|
||||
process_7z () {
|
||||
mkdir -p "${user_folder}/hashes/${rom_path}/tmp"
|
||||
echo "unzipping ${file}"
|
||||
7z x "${user_folder}${rom_path}/${file}" -o"${user_folder}/hashes/${rom_path}/tmp"
|
||||
7z e "${user_folder}${rom_path}/${file}" -o"${user_folder}/hashes/${rom_path}/tmp"
|
||||
rm "${user_folder}/hashes/${rom_path}/tmp/"*.{txt,nfo,xml,readme,README} &> /dev/null || :
|
||||
find "${user_folder}/hashes/${rom_path}/tmp/" -empty -type d -delete
|
||||
echo "hashing ${file}"
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/"* | awk '{print $1;exit}')
|
||||
firstfile=( "${user_folder}/hashes/${rom_path}/tmp/"* )
|
||||
sum=$(sha1sum "$firstfile" | awk '{print $1;exit}')
|
||||
rm -R "${user_folder}/hashes/${rom_path}/tmp/"
|
||||
printf ${sum^^} > "${user_folder}/hashes/${rom_path}/${file}.sha1"
|
||||
}
|
||||
@ -52,7 +62,7 @@ process_nes () {
|
||||
if [ $file_type == 'zip' ]; then
|
||||
mkdir -p "${user_folder}/hashes/${rom_path}/tmp"
|
||||
echo "unzipping ${file}"
|
||||
unzip -q "${user_folder}${rom_path}/${file}" -d "${user_folder}/hashes/${rom_path}/tmp"
|
||||
unzip -j -q "${user_folder}${rom_path}/${file}" -d "${user_folder}/hashes/${rom_path}/tmp"
|
||||
rm "${user_folder}/hashes/${rom_path}/tmp/"*.{txt,nfo,xml,readme,README} &> /dev/null || :
|
||||
file_to_sha="${user_folder}/hashes/${rom_path}/tmp/"*
|
||||
elif [ $file_type == 'x-7z-compressed' ]; then
|
||||
@ -66,7 +76,7 @@ process_nes () {
|
||||
file_to_sha="${user_folder}/${rom_path}/${file}"
|
||||
fi
|
||||
echo "hashing ${file}"
|
||||
sum=$(NES20Tool -operation rominfo -rom-file ${file_to_sha} |awk '/ROM SHA1/ {print $3;exit}')
|
||||
sum=$(NES20Tool -operation rominfo -rom-file ${file_to_sha} |awk '/ROM SHA1/ {print $3;exit}' || sha1sum ${file_to_sha} | awk '{print $1;exit}')
|
||||
wait
|
||||
printf ${sum^^} > "${user_folder}/hashes/${rom_path}/${file}.sha1"
|
||||
if [ -d "${user_folder}/hashes/${rom_path}/tmp" ]; then
|
||||
@ -87,12 +97,18 @@ process_chd () {
|
||||
if grep -q "TRACK 02" "${user_folder}/hashes/${rom_path}/tmp/FILE.cue"; then
|
||||
echo "${file} is multi track need to split"
|
||||
mkdir -p "${user_folder}/hashes/${rom_path}/tmp/split"
|
||||
binmerge -s "${user_folder}/hashes/${rom_path}/tmp/FILE.cue" FILE -o "${user_folder}/hashes/${rom_path}/tmp/split"
|
||||
echo "hashing ${file} (Track 1)"
|
||||
if [ -f "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 1).bin" ]; then
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 1).bin" | awk '{print $1;exit}')
|
||||
elif [ -f "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 01).bin" ];then
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 01).bin" | awk '{print $1;exit}')
|
||||
NOSPLIT=false
|
||||
binmerge -s "${user_folder}/hashes/${rom_path}/tmp/FILE.cue" FILE -o "${user_folder}/hashes/${rom_path}/tmp/split" || NOSPLIT=true
|
||||
if [ "${NOSPLIT}" == "false" ]; then
|
||||
echo "hashing ${file} (Track 1)"
|
||||
if [ -f "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 1).bin" ]; then
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 1).bin" | awk '{print $1;exit}')
|
||||
elif [ -f "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 01).bin" ];then
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/split/FILE (Track 01).bin" | awk '{print $1;exit}')
|
||||
fi
|
||||
else
|
||||
echo "splitting failed hashing full bin ${file}"
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/FILE.cue" | awk '{print $1;exit}')
|
||||
fi
|
||||
elif grep -q "TRACK 01" "${user_folder}/hashes/${rom_path}/tmp/FILE.cue"; then
|
||||
echo "hashing ${file}"
|
||||
@ -116,12 +132,18 @@ process_bin () {
|
||||
if grep -q "TRACK 02" "${user_folder}/${rom_path}/${cuefile}"; then
|
||||
echo "${file} is multi track need to split"
|
||||
mkdir -p "${user_folder}/${rom_path}/tmp/"
|
||||
binmerge -s "${user_folder}/${rom_path}/${cuefile}" FILE -o "${user_folder}/${rom_path}/tmp/"
|
||||
echo "hashing ${file} (Track 1)"
|
||||
if [ -f "${user_folder}/${rom_path}/tmp/FILE (Track 1).bin" ]; then
|
||||
sum=$(sha1sum "${user_folder}/${rom_path}/tmp/FILE (Track 1).bin" | awk '{print $1;exit}')
|
||||
elif [ -f "${user_folder}/${rom_path}/tmp/FILE (Track 01).bin" ];then
|
||||
sum=$(sha1sum "${user_folder}/${rom_path}/tmp/FILE (Track 01).bin" | awk '{print $1;exit}')
|
||||
NOSPLIT=false
|
||||
binmerge -s "${user_folder}/${rom_path}/${cuefile}" FILE -o "${user_folder}/${rom_path}/tmp/" || NOSPLIT=true
|
||||
if [ "${NOSPLIT}" == "false" ]; then
|
||||
echo "hashing ${file} (Track 1)"
|
||||
if [ -f "${user_folder}/${rom_path}/tmp/FILE (Track 1).bin" ]; then
|
||||
sum=$(sha1sum "${user_folder}/${rom_path}/tmp/FILE (Track 1).bin" | awk '{print $1;exit}')
|
||||
elif [ -f "${user_folder}/${rom_path}/tmp/FILE (Track 01).bin" ];then
|
||||
sum=$(sha1sum "${user_folder}/${rom_path}/tmp/FILE (Track 01).bin" | awk '{print $1;exit}')
|
||||
fi
|
||||
else
|
||||
echo "splitting failed hashing full bin ${file}"
|
||||
sum=$(sha1sum "${user_folder}/${rom_path}/${file}" | awk '{print $1;exit}')
|
||||
fi
|
||||
elif grep -q "TRACK 01" "${user_folder}/${rom_path}/${cuefile}"; then
|
||||
echo "hashing ${file}"
|
||||
@ -133,31 +155,46 @@ process_bin () {
|
||||
fi
|
||||
}
|
||||
|
||||
# Process special zip file hashes
|
||||
process_zip_by_name () {
|
||||
mkdir -p "${user_folder}/hashes/${rom_path}/tmp"
|
||||
echo "unzipping ${file}"
|
||||
unzip -j -q "${user_folder}${rom_path}/${file}" -d "${user_folder}/hashes/${rom_path}/tmp"
|
||||
echo "hashing ${file}"
|
||||
sum=$(sha1sum "${user_folder}/hashes/${rom_path}/tmp/${file%.*}."* | awk '{print $1;exit}')
|
||||
rm -R "${user_folder}/hashes/${rom_path}/tmp/"
|
||||
printf ${sum^^} > "${user_folder}/hashes/${rom_path}/${file}.sha1"
|
||||
}
|
||||
|
||||
IFS=$'\n'
|
||||
mkdir -p "${user_folder}/hashes/${rom_path}"
|
||||
for file in $check_files; do
|
||||
file_extension="${file##*.}"
|
||||
if [ $rom_type == 'arcade' ] || [ $rom_type == 'segaSaturn' ]; then
|
||||
process_name
|
||||
elif [ "${file_extension,,}" = 'chd' ] && [ $rom_type == 'pce' ]; then
|
||||
process_name
|
||||
elif [ "${file_extension,,}" = 'chd' ] || ([ $rom_type == 'psx' ] && [[ "${file_extension,,}" == "disk1" ]]); then
|
||||
process_chd
|
||||
elif [ "${file_extension,,}" = 'bin' ] && [ $rom_type != '3do' ]; then
|
||||
process_bin
|
||||
elif [[ "${file_extension,,}" = @(img|cue|ccd|disk*|sub) ]]; then
|
||||
echo "Filetype ${file_extension} not supported"
|
||||
elif [ $rom_type == 'nes' ]; then
|
||||
file_type=$(file -b --mime-type "${user_folder}${rom_path}/${file}" | awk -F'/' '{print $2}')
|
||||
process_nes
|
||||
else
|
||||
file_type=$(file -b --mime-type "${user_folder}${rom_path}/${file}" | awk -F'/' '{print $2}')
|
||||
if [ $file_type == 'zip' ]; then
|
||||
process_zip
|
||||
elif [ $file_type == 'x-7z-compressed' ]; then
|
||||
process_7z
|
||||
if [ ! -f "${user_folder}/hashes/${rom_path}/${file}.sha1" ]; then
|
||||
file_extension="${file##*.}"
|
||||
if [ "${file_extension,,}" = 'multiwad' ]; then
|
||||
process_zip_by_name
|
||||
elif [ $rom_type == 'arcade' ] || [ $rom_type == 'segaSaturn' ] && [[ "${file_extension,,}" != @(img|cue|ccd|disk*|sub) ]]; then
|
||||
process_name
|
||||
elif [ "${file_extension,,}" = 'chd' ] && [ $rom_type == 'pce' ]; then
|
||||
process_name
|
||||
elif [ "${file_extension,,}" = 'chd' ] || ([ $rom_type == 'psx' ] && [[ "${file_extension,,}" == "disk1" ]]); then
|
||||
process_chd
|
||||
elif [ "${file_extension,,}" = 'bin' ] && [ $rom_type != '3do' ]; then
|
||||
process_bin
|
||||
elif [[ "${file_extension,,}" = @(img|cue|ccd|disk*|sub) ]]; then
|
||||
echo "Filetype ${file_extension} not supported"
|
||||
elif [ $rom_type == 'nes' ]; then
|
||||
file_type=$(file -b --mime-type "${user_folder}${rom_path}/${file}" | awk -F'/' '{print $2}')
|
||||
process_nes
|
||||
else
|
||||
just_hash
|
||||
file_type=$(file -b --mime-type "${user_folder}${rom_path}/${file}" | awk -F'/' '{print $2}')
|
||||
if [ $file_type == 'zip' ]; then
|
||||
process_zip
|
||||
elif [ $file_type == 'x-7z-compressed' ]; then
|
||||
process_7z
|
||||
else
|
||||
just_hash
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
567
index.js
567
index.js
@ -1,4 +1,5 @@
|
||||
// NPM modules
|
||||
var home = require('os').homedir();
|
||||
var socketIO = require('socket.io');
|
||||
var fs = require('fs');
|
||||
var fsw = require('fs').promises;
|
||||
@ -11,9 +12,14 @@ var http = require('http').Server(app);
|
||||
var baserouter = express.Router();
|
||||
var { spawn } = require('child_process');
|
||||
var { create } = require('ipfs-http-client');
|
||||
var crypto = require('crypto');
|
||||
var ipfs = create();
|
||||
var merge = require('deepmerge');
|
||||
|
||||
// Default vars
|
||||
if (home == '/data') {
|
||||
home = '/config'
|
||||
}
|
||||
var baseUrl = process.env.SUBFOLDER || '/';
|
||||
if (fs.existsSync('/data')) {
|
||||
var dataRoot = '/data/';
|
||||
@ -22,7 +28,8 @@ if (fs.existsSync('/data')) {
|
||||
};
|
||||
var configPath = dataRoot + 'config/';
|
||||
var hashPath = dataRoot + 'hashes/';
|
||||
var defaultPeer = '/ip4/206.189.169.226/tcp/4001/p2p/12D3KooWHRqeK6as7tbuoaQdoTUNayUQzfY5aUtTa9GpYzdWNPqU';
|
||||
var metaPath = dataRoot + 'metadata/';
|
||||
var defaultPeer = '/ip4/65.109.29.184/tcp/4001/p2p/12D3KooWAQZgCmhRo6V6yzGWTtw57xSRBnTn5kGMqzahFKyt5CW3';
|
||||
var metaVariables = [
|
||||
['vid', 'videos', '.mp4'],
|
||||
['logo', 'logos', '.png'],
|
||||
@ -30,13 +37,47 @@ var metaVariables = [
|
||||
['corner', 'corners', '.png']
|
||||
];
|
||||
var emus = [
|
||||
'3do', 'arcade', 'atari2600', 'atari7800', 'colecovision',
|
||||
'doom', 'gb', 'gba', 'gbc', 'jaguar', 'lynx',
|
||||
'msx', 'n64', 'nds', 'nes', 'ngp', 'odyssey2',
|
||||
'pce', 'psx', 'sega32x', 'segaCD',
|
||||
'segaGG', 'segaMD', 'segaMS', 'segaSG',
|
||||
'segaSaturn', 'snes', 'vb', 'vectrex', 'ws'
|
||||
{'name': '3do', 'video_position': 'left:11.5vw;top:30vh;width:36.3vw;height:45.5vh;'},
|
||||
{'name': 'arcade', 'video_position': 'left:10.3vw;top:30.5vh;width:36.5vw;height:48vh;'},
|
||||
{'name': 'atari2600', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'atari5200', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'atari7800', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'colecovision', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'doom', 'video_position': 'left:11.5vw;top:30vh;width:36.3vw;height:45.5vh;'},
|
||||
{'name': 'gb', 'video_position': 'left:14.5vw;top:31vh;width:26vw;height:43.5vh;'},
|
||||
{'name': 'gba', 'video_position': 'left:13.5vw;top:36vh;width:31.7vw;height:38.3vh;'},
|
||||
{'name': 'gbc', 'video_position': 'left:15.5vw;top:31.2vh;width:28vw;height:44.7vh;'},
|
||||
{'name': 'jaguar', 'video_position': 'left:11.5vw;top:30vh;width:36.3vw;height:45.5vh;'},
|
||||
{'name': 'lynx', 'video_position': 'left:11vw;top:31vh;width:36vw;height:44vh;'},
|
||||
{'name': 'msx', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'n64', 'video_position': 'left:11.5vw;top:30vh;width:36.3vw;height:45.5vh;'},
|
||||
{'name': 'nds', 'video_position': 'left:23.8vw;top:25.7vh;width:20vw;height:56vh;'},
|
||||
{'name': 'nes', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'ngp', 'video_position': 'left:15vw;top:34vh;width:25vw;height:40vh;'},
|
||||
{'name': 'odyssey2', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'pce', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'psx', 'video_position': 'left:11.5vw;top:30vh;width:36.3vw;height:45.5vh;'},
|
||||
{'name': 'sega32x', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'segaCD', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'segaGG', 'video_position': 'left:12.3vw;top:31.5vh;width:33.4vw;height:43.3vh;'},
|
||||
{'name': 'segaMD', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'segaMS', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'segaSaturn', 'video_position': 'left:11.5vw;top:30vh;width:36.3vw;height:45.5vh;'},
|
||||
{'name': 'segaSG', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'snes', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'vb', 'video_position': 'left:11.5vw;top:31.5vh;width:36vw;height:43vh;'},
|
||||
{'name': 'vectrex', 'video_position': 'left:18vw;top:30vh;width:22vw;height:46vh;'},
|
||||
{'name': 'ws', 'video_position': 'left:11.5vw;top:31vh;width:35vw;height:43vh;'}
|
||||
];
|
||||
var retroArchCfg = `
|
||||
input_menu_toggle_gamepad_combo = 3
|
||||
system_directory = /home/web_user/retroarch/system/`
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
res.header("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
res.header("Cross-Origin-Opener-Policy", "same-origin");
|
||||
next();
|
||||
});
|
||||
|
||||
//// Http server ////
|
||||
baserouter.use('/public', express.static(__dirname + '/public'));
|
||||
@ -48,7 +89,7 @@ app.use(baseUrl, baserouter);
|
||||
http.listen(3000);
|
||||
|
||||
//// socketIO comms ////
|
||||
io = socketIO(http, {path: baseUrl + 'socket.io'});
|
||||
io = socketIO(http, {path: baseUrl + 'socket.io',maxHttpBufferSize: 100000000});
|
||||
io.on('connection', async function (socket) {
|
||||
//// Functions ////
|
||||
// Send config list to client
|
||||
@ -56,31 +97,46 @@ io.on('connection', async function (socket) {
|
||||
var files = await fsw.readdir(configPath);
|
||||
socket.emit('renderconfigs', files);
|
||||
};
|
||||
|
||||
// Send MetaData list to client
|
||||
async function renderMeta() {
|
||||
await fsw.mkdir(metaPath, { recursive: true });
|
||||
var files = await fsw.readdir(metaPath);
|
||||
socket.emit('rendermeta', files);
|
||||
};
|
||||
|
||||
// Send rom directories to client
|
||||
async function renderRomsDir() {
|
||||
let dirData = [];
|
||||
if (fs.existsSync(hashPath)) {
|
||||
var dirs = await fsw.readdir(hashPath);
|
||||
socket.emit('renderromsdir', dirs);
|
||||
let dirs = await fsw.readdir(hashPath);
|
||||
for await (let dir of dirs) {
|
||||
if (fs.lstatSync(hashPath + dir).isDirectory()) {
|
||||
dirData.push(dir);
|
||||
}
|
||||
}
|
||||
socket.emit('renderromsdir', dirData);
|
||||
} else {
|
||||
socket.emit('renderromsdir', []);
|
||||
socket.emit('renderromsdir', dirData);
|
||||
};
|
||||
};
|
||||
|
||||
// Tell client to render rom scanners
|
||||
async function renderRoms() {
|
||||
var romData = {}
|
||||
for await (var emu of emus) {
|
||||
var romCount = 0;
|
||||
var hashCount = 0;
|
||||
var romPath = dataRoot + emu + '/roms/';
|
||||
if (fs.existsSync(hashPath + emu)) {
|
||||
var hashes = await fsw.readdir(hashPath + emu + '/roms/');
|
||||
var romPath = dataRoot + emu.name + '/roms/';
|
||||
if (fs.existsSync(hashPath + emu.name)) {
|
||||
var hashes = await fsw.readdir(hashPath + emu.name + '/roms/');
|
||||
var hashCount = hashes.length;
|
||||
};
|
||||
if (fs.existsSync(romPath)) {
|
||||
var roms = await fsw.readdir(romPath);
|
||||
var romCount = roms.length;
|
||||
};
|
||||
romData[emu] = {'roms': romCount,'hashes': hashCount};
|
||||
romData[emu.name] = {'roms': romCount,'hashes': hashCount};
|
||||
};
|
||||
// Grab default files data
|
||||
var defaultFiles = await fsw.readFile('./metadata/default_files.json', 'utf8');
|
||||
@ -96,49 +152,54 @@ io.on('connection', async function (socket) {
|
||||
renderRomsDir();
|
||||
socket.emit('renderromslanding', romData);
|
||||
};
|
||||
|
||||
// Tell client to render landing
|
||||
function renderLanding() {
|
||||
socket.emit('renderlanding');
|
||||
};
|
||||
|
||||
// Send file contents to client
|
||||
async function getConfig(file) {
|
||||
var file = file + '.json';
|
||||
var fileContents = await fsw.readFile(configPath + file, 'utf8');
|
||||
file = file + '.json';
|
||||
let fileContents = await fsw.readFile(configPath + file, 'utf8');
|
||||
socket.emit('renderconfig', JSON.parse(fileContents));
|
||||
};
|
||||
|
||||
// Send meta contents to client
|
||||
async function getMetaJSON(file) {
|
||||
file = file + '.json';
|
||||
let fileContents = await fsw.readFile(metaPath + file, 'utf8');
|
||||
socket.emit('rendermetajson', JSON.parse(fileContents));
|
||||
};
|
||||
|
||||
// Save sent config file
|
||||
async function saveConfig(data) {
|
||||
var fileName = data.name + '.json';
|
||||
configFile = JSON.stringify(data.config, null, 2);
|
||||
await fsw.writeFile(configPath + fileName, configFile);
|
||||
};
|
||||
|
||||
// Organize rom data to send to client
|
||||
async function getRoms(dir) {
|
||||
var metaData = await fsw.readFile('./metadata/' + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(metaData);
|
||||
if (fs.existsSync(dataRoot + 'metadata/' + dir + '.json')) {
|
||||
var userMetaData = await fsw.readFile(dataRoot + 'metadata/' + dir + '.json', 'utf8');
|
||||
var userMetaData = JSON.parse(userMetaData);
|
||||
Object.assign(metaData, userMetaData);
|
||||
};
|
||||
var shaPath = hashPath + dir + '/roms/';
|
||||
var files = await fsw.readdir(shaPath);
|
||||
var identified = {};
|
||||
var unidentified = {};
|
||||
var metaVars = [];
|
||||
let metaData = await getMeta(dir);
|
||||
let shaPath = hashPath + dir + '/roms/';
|
||||
let files = await fsw.readdir(shaPath);
|
||||
let identified = {};
|
||||
let unidentified = {};
|
||||
let metaVars = [];
|
||||
metaVars.push(...metaVariables);
|
||||
metaVars.push(['video_position', 'videos', '.position']);
|
||||
for (var file of files) {
|
||||
var fileName = file.replace('.sha1','');
|
||||
var fileExtension = path.extname(fileName);
|
||||
var name = path.basename(fileName, fileExtension);
|
||||
let fileName = file.replace('.sha1','');
|
||||
let fileExtension = path.extname(fileName);
|
||||
let name = path.basename(fileName, fileExtension);
|
||||
var sha = await fsw.readFile(shaPath + file, 'utf8');
|
||||
if (metaData.hasOwnProperty(sha)) {
|
||||
if (metaData[sha].hasOwnProperty('ref')) {
|
||||
var sha = metaData[sha].ref;
|
||||
};
|
||||
identified[fileName] = {'has_art': 'none'};
|
||||
for await (var variable of metaVars) {
|
||||
for await (let variable of metaVars) {
|
||||
if (metaData[sha].hasOwnProperty(variable[0])) {
|
||||
if (fs.existsSync(dataRoot + dir + '/' + variable[1] + '/' + name + variable[2])) {
|
||||
identified[fileName] = {'has_art': true};
|
||||
@ -152,7 +213,6 @@ io.on('connection', async function (socket) {
|
||||
unidentified[fileName] = sha;
|
||||
};
|
||||
};
|
||||
var nameList = {};
|
||||
socket.emit('renderrom', [identified, unidentified, metaData]);
|
||||
}
|
||||
// IPFS downloading
|
||||
@ -162,7 +222,7 @@ io.on('connection', async function (socket) {
|
||||
let writeStream = fs.createWriteStream(file);
|
||||
socket.emit('modaldata', 'Downloading: ' + file);
|
||||
try {
|
||||
for await (var fileStream of ipfs.cat(cid, {'timeout': 10000})) {
|
||||
for await (var fileStream of ipfs.cat(cid, {'timeout': 20000})) {
|
||||
writeStream.write(fileStream);
|
||||
};
|
||||
writeStream.end();
|
||||
@ -187,10 +247,12 @@ io.on('connection', async function (socket) {
|
||||
};
|
||||
return '';
|
||||
};
|
||||
|
||||
// Set default ipfs peer if DL times out
|
||||
async function ipfsDefaultPeer() {
|
||||
await ipfs.swarm.connect(defaultPeer);
|
||||
};
|
||||
|
||||
// Download default files for user directory
|
||||
async function dlDefaultFiles() {
|
||||
var metaData = await fsw.readFile('./metadata/default_files.json', 'utf8');
|
||||
@ -206,22 +268,25 @@ io.on('connection', async function (socket) {
|
||||
}
|
||||
};
|
||||
for await (var dir of emus) {
|
||||
var path = dataRoot + 'hashes/' + dir + '/roms/';
|
||||
var path = dataRoot + 'hashes/' + dir.name + '/roms/';
|
||||
if (fs.existsSync(path)) {
|
||||
var roms = await fsw.readdir(path);
|
||||
if (roms.length > 0) {
|
||||
socket.emit('modaldata', 'Processing Config for ' + dir);
|
||||
await addToConfig(dir, true);
|
||||
socket.emit('modaldata', 'Processing Config for ' + dir.name);
|
||||
await addToConfig(dir.name, true);
|
||||
};
|
||||
};
|
||||
};
|
||||
socket.emit('modaldata', 'Downloaded All Files');
|
||||
renderRoms();
|
||||
};
|
||||
|
||||
// Scan roms directory using helper script
|
||||
function scanRoms(folder) {
|
||||
function scanRoms(data) {
|
||||
let folder = data[0];
|
||||
let fullScan = data[1];
|
||||
socket.emit('emptymodal');
|
||||
var scanProcess = spawn('./has_files.sh', ['/' + folder + '/roms/', folder]);
|
||||
let scanProcess = spawn('./has_files.sh', ['/' + folder + '/roms/', folder, fullScan]);
|
||||
scanProcess.stdout.setEncoding('utf8');
|
||||
scanProcess.stderr.setEncoding('utf8');
|
||||
scanProcess.stdout.on('data', function(data) {
|
||||
@ -232,20 +297,26 @@ io.on('connection', async function (socket) {
|
||||
});
|
||||
scanProcess.on('close', function(code) {
|
||||
socket.emit('modaldata', 'Scan exited with code: ' + code);
|
||||
renderRoms();
|
||||
if (fullScan) {
|
||||
renderRoms();
|
||||
} else {
|
||||
getRoms(folder);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add roms to config file
|
||||
async function addToConfig(dir, render) {
|
||||
// For arcade roms we need clone info
|
||||
if (dir == 'arcade') {
|
||||
var metaData = await fsw.readFile('./metadata/' + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(metaData);
|
||||
var metaData = await getMeta(dir);
|
||||
};
|
||||
var configFile = configPath + dir + '.json';
|
||||
var shaPath = hashPath + dir + '/roms/';
|
||||
var files = await fsw.readdir(shaPath);
|
||||
var config = await fsw.readFile(configFile, 'utf8');
|
||||
var config = JSON.parse(config);
|
||||
// Update config file with current rom files
|
||||
let configFile = configPath + dir + '.json';
|
||||
let shaPath = hashPath + dir + '/roms/';
|
||||
let files = await fsw.readdir(shaPath);
|
||||
let config = await fsw.readFile(configFile, 'utf8');
|
||||
config = JSON.parse(config);
|
||||
config.items = {};
|
||||
if (files.length < 9) {
|
||||
var itemsLength = files.length;
|
||||
@ -253,15 +324,15 @@ io.on('connection', async function (socket) {
|
||||
var itemsLength = 9;
|
||||
};
|
||||
config.display_items = itemsLength;
|
||||
for await (var file of files) {
|
||||
var fileName = file.replace('.sha1','');
|
||||
var fileExtension = path.extname(fileName);
|
||||
var name = path.basename(fileName, fileExtension);
|
||||
var has_logo = fs.existsSync(dataRoot + dir + '/logos/' + name + '.png');
|
||||
var has_back = fs.existsSync(dataRoot + dir + '/backgrounds/' + name + '.png');
|
||||
var has_corner = fs.existsSync(dataRoot + dir + '/corners/' + name + '.png');
|
||||
var has_video = fs.existsSync(dataRoot + dir + '/videos/' + name + '.mp4');
|
||||
var positionFile = dataRoot + dir + '/videos/' + name + '.position';
|
||||
for await (let file of files) {
|
||||
let fileName = file.replace('.sha1','');
|
||||
let fileExtension = path.extname(fileName);
|
||||
let name = path.basename(fileName, fileExtension);
|
||||
let has_logo = fs.existsSync(dataRoot + dir + '/logos/' + name + '.png');
|
||||
let has_back = fs.existsSync(dataRoot + dir + '/backgrounds/' + name + '.png');
|
||||
let has_corner = fs.existsSync(dataRoot + dir + '/corners/' + name + '.png');
|
||||
let has_video = fs.existsSync(dataRoot + dir + '/videos/' + name + '.mp4');
|
||||
let positionFile = dataRoot + dir + '/videos/' + name + '.position';
|
||||
config.items[name] = {};
|
||||
var multi_disc = 0;
|
||||
if (fileExtension == '.disk1') {
|
||||
@ -281,7 +352,8 @@ io.on('connection', async function (socket) {
|
||||
Object.assign(config.items[name], {'multi_disc': multi_disc});
|
||||
};
|
||||
if (fs.existsSync(positionFile)) {
|
||||
var video_position = await fsw.readFile(positionFile, 'utf8');
|
||||
let video_position = await fsw.readFile(positionFile, 'utf8');
|
||||
Object.assign(config.items[name], {'video_position': video_position});
|
||||
};
|
||||
if (has_logo !== config.defaults.has_logo) {
|
||||
Object.assign(config.items[name], {'has_logo': has_logo});
|
||||
@ -298,27 +370,58 @@ io.on('connection', async function (socket) {
|
||||
if (fileExtension !== config.defaults.rom_extension) {
|
||||
Object.assign(config.items[name], {'rom_extension': fileExtension});
|
||||
};
|
||||
if ((video_position) && (video_position !== config.defaults.video_position)) {
|
||||
Object.assign(config.items[name], {'video_position': video_position});
|
||||
};
|
||||
};
|
||||
var configContents = JSON.stringify(config, null, 2);
|
||||
await fsw.writeFile(configFile, configContents);
|
||||
// Update main to include stuff with roms
|
||||
var mainFile = configPath + 'main.json';
|
||||
var main = await fsw.readFile(mainFile, 'utf8');
|
||||
var main = JSON.parse(main);
|
||||
main.items = {};
|
||||
for await (var emu of emus) {
|
||||
var emuPath = dataRoot + 'hashes/' + emu.name + '/roms/';
|
||||
if (fs.existsSync(emuPath)) {
|
||||
var roms = await fsw.readdir(emuPath);
|
||||
if (roms.length > 0) {
|
||||
main.items[emu.name] = {'video_position': emu.video_position};
|
||||
};
|
||||
};
|
||||
};
|
||||
if (Object.keys(main.items).length < 9) {
|
||||
main.display_items = Object.keys(main.items).length;
|
||||
} else {
|
||||
main.display_items = 9;
|
||||
};
|
||||
var mainContents = JSON.stringify(main, null, 2);
|
||||
await fsw.writeFile(mainFile, mainContents);
|
||||
// Render page for user if needed
|
||||
if (render) {
|
||||
return '';
|
||||
} else {
|
||||
renderRoms();
|
||||
};
|
||||
};
|
||||
|
||||
// Remove config items without art
|
||||
async function purgeNoArt(dir) {
|
||||
var configFile = configPath + dir + '.json';
|
||||
var config = await fsw.readFile(configFile, 'utf8');
|
||||
var config = JSON.parse(config);
|
||||
for await (let item of Object.keys(config.items)) {
|
||||
if ((config.items[item].hasOwnProperty('has_logo')) || (config.items[item].hasOwnProperty('has_video'))) {
|
||||
if ((config.items[item].has_logo == false) || (config.items[item].has_video == false)) {
|
||||
delete config.items[item];
|
||||
}
|
||||
}
|
||||
}
|
||||
var configContents = JSON.stringify(config, null, 2);
|
||||
await fsw.writeFile(configFile, configContents);
|
||||
renderRoms();
|
||||
}
|
||||
|
||||
// Download art assets from IPFS
|
||||
async function downloadArt(dir) {
|
||||
var metaData = await fsw.readFile('./metadata/' + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(metaData);
|
||||
if (fs.existsSync('/data/metadata/' + dir + '.json')) {
|
||||
var userMetaData = await fsw.readFile(dataRoot + 'metadata/' + dir + '.json', 'utf8');
|
||||
var userMetaData = JSON.parse(userMetaData);
|
||||
Object.assign(metaData, userMetaData);
|
||||
};
|
||||
var metaData = await getMeta(dir);
|
||||
var shaPath = hashPath + dir + '/roms/';
|
||||
var files = await fsw.readdir(shaPath);
|
||||
socket.emit('emptymodal');
|
||||
@ -336,33 +439,91 @@ io.on('connection', async function (socket) {
|
||||
await ipfsDownload(metaData[sha][variable[0]], dataRoot + dir + '/' + variable[1] + '/' + name + variable[2], 0)
|
||||
};
|
||||
};
|
||||
if (metaData[sha].hasOwnProperty('position')) {
|
||||
await fsw.writeFile(dataRoot + dir + '/videos/' + name + '.position', metaData[sha].position);
|
||||
if (metaData[sha].hasOwnProperty('video_position')) {
|
||||
await fsw.writeFile(dataRoot + dir + '/videos/' + name + '.position', metaData[sha].video_position);
|
||||
};
|
||||
};
|
||||
};
|
||||
socket.emit('modaldata', 'Downloaded All Files');
|
||||
getRoms(dir);
|
||||
};
|
||||
|
||||
// Set user linked metadata
|
||||
async function userMeta(data) {
|
||||
await fsw.mkdir(dataRoot + 'metadata', { recursive: true })
|
||||
var romSha = data[0];
|
||||
var linkSha = data[1];
|
||||
var dir = data[2];
|
||||
if (fs.existsSync(dataRoot + 'metadata/' + dir + '.json')) {
|
||||
var metaData = await fsw.readFile(dataRoot + 'metadata/' + dir + '.json', 'utf8');
|
||||
await fsw.mkdir(metaPath, { recursive: true });
|
||||
let romSha = data[0];
|
||||
let linkSha = data[1];
|
||||
let dir = data[2];
|
||||
if (fs.existsSync(metaPath + dir + '.json')) {
|
||||
var metaData = await fsw.readFile(metaPath + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(metaData);
|
||||
} else {
|
||||
var metaData = {};
|
||||
};
|
||||
var link = {};
|
||||
let link = {};
|
||||
link[romSha] = {'ref': linkSha};
|
||||
Object.assign(metaData, link);
|
||||
userMetadataFile = JSON.stringify(metaData, null, 2);
|
||||
await fsw.writeFile(dataRoot + 'metadata/' + dir + '.json', userMetadataFile);
|
||||
await fsw.writeFile(metaPath + dir + '.json', userMetadataFile);
|
||||
getRoms(dir);
|
||||
};
|
||||
|
||||
// Remove user linked metadata
|
||||
async function removeMeta(data) {
|
||||
await fsw.mkdir(metaPath, { recursive: true });
|
||||
let romSha = data[0];
|
||||
let dir = data[1];
|
||||
let file = data[2];
|
||||
let purge = data[3];
|
||||
let fileExtension = path.extname(file);
|
||||
let name = path.basename(file, fileExtension);
|
||||
if (fs.existsSync(metaPath + dir + '.json')) {
|
||||
var metaData = await fsw.readFile(metaPath + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(metaData);
|
||||
} else {
|
||||
var metaData = {};
|
||||
};
|
||||
if (metaData.hasOwnProperty(romSha)) {
|
||||
delete metaData[romSha];
|
||||
userMetadataFile = JSON.stringify(metaData, null, 2);
|
||||
await fsw.writeFile(metaPath + dir + '.json', userMetadataFile);
|
||||
}
|
||||
// Delete any downloaded or uploaded art
|
||||
for await (let variable of metaVariables) {
|
||||
let artFile = dataRoot + dir + '/' + variable[1] + '/' + name + variable[2];
|
||||
if (fs.existsSync(artFile)) {
|
||||
fs.unlinkSync(artFile);
|
||||
}
|
||||
};
|
||||
let vidPosFile = dataRoot + dir + '/videos/' + name + '.position';
|
||||
if (fs.existsSync(vidPosFile)) {
|
||||
fs.unlinkSync(vidPosFile);
|
||||
}
|
||||
// Delete rom and sha if requested
|
||||
if (purge) {
|
||||
let romFile = dataRoot + dir + '/roms/' + file;
|
||||
let shaFile = dataRoot + 'hashes/' + dir + '/roms/' + file + '.sha1';
|
||||
for await (var delFile of [romFile, shaFile]) {
|
||||
if (fs.existsSync(delFile)) {
|
||||
fs.unlinkSync(delFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tell client to render
|
||||
getRoms(dir);
|
||||
};
|
||||
|
||||
// Get combined metadata
|
||||
async function getMeta(dir) {
|
||||
let metaDataRaw = await fsw.readFile('./metadata/' + dir + '.json', 'utf8');
|
||||
let metaData = JSON.parse(metaDataRaw);
|
||||
if (fs.existsSync(metaPath + dir + '.json')) {
|
||||
let userMetaDataRaw = await fsw.readFile(metaPath + dir + '.json', 'utf8');
|
||||
let userMetaData = JSON.parse(userMetaDataRaw);
|
||||
metaData = merge(metaData, userMetaData);
|
||||
};
|
||||
return metaData;
|
||||
}
|
||||
|
||||
// Render files page
|
||||
async function renderFiles() {
|
||||
@ -376,45 +537,230 @@ io.on('connection', async function (socket) {
|
||||
socket.emit('renderfiledirs', dirs);
|
||||
};
|
||||
|
||||
// Delete rom
|
||||
async function deleteRom(data) {
|
||||
var fileName = data[0].replace("|","'");
|
||||
var dir = data[1];
|
||||
var fileExtension = path.extname(fileName);
|
||||
var name = path.basename(fileName, fileExtension);
|
||||
let metaVars = [];
|
||||
metaVars.push(...metaVariables);
|
||||
metaVars.push(['video_position', 'videos', '.position']);
|
||||
for await (var variable of metaVars) {
|
||||
var file = dataRoot + dir + '/' + variable[1] + '/' + name + variable[2];
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
};
|
||||
};
|
||||
var romFile = dataRoot + dir + '/roms/' + fileName;
|
||||
var shaFile = dataRoot + 'hashes/' + dir + '/roms/' + fileName + '.sha1';
|
||||
for await (var file of [romFile, shaFile]) {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
};
|
||||
// Send profile data to client
|
||||
async function renderProfiles() {
|
||||
let profilesData = await fsw.readFile(home + '/profile/profile.json', 'utf8');
|
||||
let profilesJson = JSON.parse(profilesData);
|
||||
let profiles = [];
|
||||
if (Object.keys(profilesJson).length > 0) {
|
||||
for await (let profile of Object.keys(profilesJson)) {
|
||||
profiles.push(profilesJson[profile].username)
|
||||
}
|
||||
}
|
||||
socket.emit('renderprofiles', profiles);
|
||||
}
|
||||
|
||||
// Create a blank profile
|
||||
async function createProfile(data) {
|
||||
let user = data[0];
|
||||
let pass = data[1];
|
||||
let auth = user + pass;
|
||||
// Create hash and store user in profile.json
|
||||
let hash = crypto.createHash('sha256').update(auth).digest('hex');
|
||||
let profilesData = await fsw.readFile(home + '/profile/profile.json', 'utf8');
|
||||
let profilesJson = JSON.parse(profilesData);
|
||||
profilesJson[hash] = {username: user};
|
||||
profileFile = JSON.stringify(profilesJson, null, 2);
|
||||
await fsw.writeFile(home + '/profile/profile.json', profileFile);
|
||||
// Make directory for user with default config
|
||||
await fsw.mkdir(home + '/profile/' + user);
|
||||
await fsw.writeFile(home + '/profile/' + user + '/retroarch.cfg', retroArchCfg);
|
||||
// Tell client to render profiles
|
||||
let profiles = [];
|
||||
for await (let profile of Object.keys(profilesJson)) {
|
||||
profiles.push(profilesJson[profile].username)
|
||||
}
|
||||
socket.emit('renderprofiles', profiles);
|
||||
}
|
||||
|
||||
// Delete a profile
|
||||
async function deleteProfile(user) {
|
||||
let profilesData = await fsw.readFile(home + '/profile/profile.json', 'utf8');
|
||||
let profilesJson = JSON.parse(profilesData);
|
||||
for await (let profile of Object.keys(profilesJson)) {
|
||||
if (profilesJson[profile].username == user) {
|
||||
delete profilesJson[profile];
|
||||
}
|
||||
}
|
||||
profileFile = JSON.stringify(profilesJson, null, 2);
|
||||
await fsw.writeFile(home + '/profile/profile.json', profileFile);
|
||||
if (user !== 'default') {
|
||||
await fsw.rm(home + '/profile/' + user, { recursive: true, force: true });
|
||||
}
|
||||
let profiles = [];
|
||||
for await (let profile of Object.keys(profilesJson)) {
|
||||
profiles.push(profilesJson[profile].username)
|
||||
}
|
||||
socket.emit('renderprofiles', profiles);
|
||||
}
|
||||
|
||||
// Send individual rom data
|
||||
async function getRomData(data) {
|
||||
let dir = data[0];
|
||||
let file = data[1];
|
||||
let fileExtension = path.extname(file);
|
||||
let name = path.basename(file, fileExtension);
|
||||
// Create the preview json
|
||||
let rawConfig = await fsw.readFile(dataRoot + 'config/' + dir + '.json', 'utf8');
|
||||
let config = JSON.parse(rawConfig);
|
||||
let defaultVidPos = config.defaults.video_position;
|
||||
config.display_items = 1;
|
||||
config.items = {};
|
||||
config.items[name] = {};
|
||||
// Assemble metdata for client
|
||||
let romData = {};
|
||||
romData.file = file;
|
||||
let hash = await fsw.readFile(hashPath + dir + '/roms/' + file + '.sha1', 'utf8');
|
||||
romData.hash = hash;
|
||||
let metaData = await getMeta(dir);
|
||||
if (metaData.hasOwnProperty(hash)) {
|
||||
if (metaData[hash].hasOwnProperty('ref')) {
|
||||
romData.metadata = metaData[metaData[hash].ref];
|
||||
if (metaData[hash].hasOwnProperty('name')) {
|
||||
romData.metadata.name = metaData[hash].name;
|
||||
}
|
||||
} else {
|
||||
romData.metadata = metaData[hash];
|
||||
}
|
||||
if (metaData[hash].hasOwnProperty('video_position')) {
|
||||
romData.vidpos = true;
|
||||
romData.metadata.video_position = metaData[hash].video_position;
|
||||
config.items[name].video_position = metaData[hash].video_position;
|
||||
} else {
|
||||
romData.vidpos = false;
|
||||
romData.metadata.video_position = defaultVidPos;
|
||||
config.items[name].video_position = defaultVidPos;
|
||||
}
|
||||
} else {
|
||||
romData.metadata = false;
|
||||
}
|
||||
for await (let variable of metaVariables) {
|
||||
if (fs.existsSync(dataRoot + dir + '/' + variable[1] + '/' + name + variable[2])) {
|
||||
romData[variable[0]] = dir + '/' + variable[1] + '/' + name + variable[2];
|
||||
config.items[name]['has_' + variable[0].replace('vid','video')] = true;
|
||||
} else {
|
||||
romData[variable[0]] = false;
|
||||
config.items[name]['has_' + variable[0].replace('vid','video')] = false;
|
||||
}
|
||||
}
|
||||
// Write the preview config
|
||||
await fsw.writeFile(hashPath + 'preview.json', JSON.stringify(config, null, 2));
|
||||
// Send data to client
|
||||
socket.emit('romdata', romData);
|
||||
}
|
||||
|
||||
// Process upload and add to custom user metadata
|
||||
async function uploadArt(data) {
|
||||
// Parse vars
|
||||
let fileType = data[0];
|
||||
let dir = data[1];
|
||||
let file = data[2];
|
||||
let hash = data[3];
|
||||
// Upload data to local ipfs node
|
||||
let fileData = Buffer.from(data[4]);
|
||||
let ipfsRes = await ipfs.add(fileData);
|
||||
let cid = ipfsRes.path;
|
||||
await ipfs.pin.add(cid);
|
||||
// Build and write custom metadata
|
||||
if (fs.existsSync(metaPath + dir + '.json')) {
|
||||
let rawMetaData = await fsw.readFile(metaPath + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(rawMetaData);
|
||||
} else {
|
||||
var metaData = {};
|
||||
}
|
||||
let metaUpdate = {};
|
||||
metaUpdate[hash] = {};
|
||||
metaUpdate[hash][fileType] = cid;
|
||||
metaData = merge(metaData, metaUpdate);
|
||||
await fsw.writeFile(metaPath + dir + '.json', JSON.stringify(metaData, null, 2));
|
||||
// Write actual file
|
||||
if (fileType == 'vid') {
|
||||
var extension = '.mp4';
|
||||
var filePath = '/videos/';
|
||||
} else if (fileType == 'back') {
|
||||
var extension = '.png';
|
||||
var filePath = '/backgrounds/';
|
||||
} else if (fileType == 'corner') {
|
||||
var extension = '.png';
|
||||
var filePath = '/corners/';
|
||||
} else if (fileType == 'logo') {
|
||||
var extension = '.png';
|
||||
var filePath = '/logos/';
|
||||
}
|
||||
let fileExtension = path.extname(file);
|
||||
let name = path.basename(file, fileExtension);
|
||||
if (! fs.existsSync(dataRoot + dir + filePath)) {
|
||||
await fsw.mkdir(dataRoot + dir + filePath);
|
||||
}
|
||||
await fsw.writeFile(dataRoot + dir + filePath + name + extension, fileData);
|
||||
// Render game for client
|
||||
getRomData([dir, file]);
|
||||
}
|
||||
|
||||
// Update video position metadata for rom
|
||||
async function updateVidPosition(data) {
|
||||
let dir = data[0];
|
||||
let file = data[1];
|
||||
let hash = data[2];
|
||||
let position = data[3];
|
||||
// Build and write custom metadata
|
||||
if (fs.existsSync(metaPath + dir + '.json')) {
|
||||
let rawMetaData = await fsw.readFile(metaPath + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(rawMetaData);
|
||||
} else {
|
||||
var metaData = {};
|
||||
}
|
||||
let metaUpdate = {};
|
||||
metaUpdate[hash] = {};
|
||||
metaUpdate[hash].video_position = position;
|
||||
metaData = merge(metaData, metaUpdate);
|
||||
await fsw.writeFile(metaPath + dir + '.json', JSON.stringify(metaData, null, 2));
|
||||
getRomData([dir, file]);
|
||||
}
|
||||
|
||||
// Create a custom metadata entry
|
||||
async function customMeta(data) {
|
||||
await fsw.mkdir(metaPath, { recursive: true });
|
||||
let romSha = data[0];
|
||||
let dir = data[1];
|
||||
let name = data[2];
|
||||
if (fs.existsSync(metaPath + dir + '.json')) {
|
||||
var metaData = await fsw.readFile(metaPath + dir + '.json', 'utf8');
|
||||
var metaData = JSON.parse(metaData);
|
||||
} else {
|
||||
var metaData = {};
|
||||
};
|
||||
metaData[romSha] = {};
|
||||
metaData[romSha].name = name;
|
||||
userMetadataFile = JSON.stringify(metaData, null, 2);
|
||||
await fsw.writeFile(metaPath + dir + '.json', userMetadataFile);
|
||||
getRoms(dir);
|
||||
};
|
||||
}
|
||||
|
||||
// Incoming socket requests
|
||||
socket.on('renderconfigs', renderConfigs);
|
||||
socket.on('renderroms', renderRoms);
|
||||
socket.on('renderromsdir', renderRomsDir);
|
||||
socket.on('getconfig', getConfig);
|
||||
socket.on('getmeta', getMetaJSON);
|
||||
socket.on('getroms', getRoms);
|
||||
socket.on('saveconfig', saveConfig);
|
||||
socket.on('dldefaultfiles', dlDefaultFiles);
|
||||
socket.on('scanroms', scanRoms);
|
||||
socket.on('addtoconfig', addToConfig);
|
||||
socket.on('purgenoart', purgeNoArt);
|
||||
socket.on('downloadart', downloadArt);
|
||||
socket.on('usermeta', userMeta);
|
||||
socket.on('renderfiles', renderFiles);
|
||||
socket.on('deleterom', deleteRom);
|
||||
socket.on('renderprofiles', renderProfiles);
|
||||
socket.on('createprofile', createProfile);
|
||||
socket.on('deleteprofile', deleteProfile);
|
||||
socket.on('getromdata', getRomData);
|
||||
socket.on('uploadart', uploadArt);
|
||||
socket.on('updatevidposition', updateVidPosition);
|
||||
socket.on('removemeta', removeMeta);
|
||||
socket.on('custommeta', customMeta);
|
||||
socket.on('rendermeta', renderMeta);
|
||||
// Render landing page
|
||||
if (fs.existsSync(dataRoot + 'config/main.json')) {
|
||||
renderRoms();
|
||||
@ -423,7 +769,7 @@ io.on('connection', async function (socket) {
|
||||
};
|
||||
});
|
||||
|
||||
// Cloudcmd File browser
|
||||
// Cloudcmd File browser data
|
||||
baserouter.use('/files', cloudcmd({
|
||||
config: {
|
||||
root: dataRoot,
|
||||
@ -441,3 +787,20 @@ baserouter.use('/files', cloudcmd({
|
||||
}
|
||||
}));
|
||||
|
||||
// Cloudcmd File browser profile
|
||||
baserouter.use('/profile', cloudcmd({
|
||||
config: {
|
||||
root: home + '/profile',
|
||||
prefix: baseUrl + 'profile',
|
||||
terminal: false,
|
||||
console: false,
|
||||
configDialog: false,
|
||||
contact: false,
|
||||
auth: false,
|
||||
name: 'Files',
|
||||
log: false,
|
||||
keysPanel: false,
|
||||
oneFilePanel: true,
|
||||
zip: false
|
||||
}
|
||||
}));
|
||||
|
||||
4576
metadata/3do.json
4576
metadata/3do.json
File diff suppressed because it is too large
Load Diff
46438
metadata/arcade.json
46438
metadata/arcade.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
367
metadata/atari5200.json
Normal file
367
metadata/atari5200.json
Normal file
@ -0,0 +1,367 @@
|
||||
{
|
||||
"18697DD8E640FA1578B3B4AE09AF484C14FB8410": {
|
||||
"logo": "QmSJBF5SmRF17pvFzsRTSGeh1gmWsGh1HiULWUVh9G3pMe",
|
||||
"name": "Asteroids (USA)",
|
||||
"vid": "Qmc98EC4j5G5BgRLHVHNDw6se8D54v5xT48GxMRkA1iJud"
|
||||
},
|
||||
"67E405C37E55DEC4071018E8166AB4FAF9F21DF5": {
|
||||
"logo": "QmURfdssjDm9P6KcxaMaLXKvBw8vzDQdZiCuBj8Ct8kXPJ",
|
||||
"name": "Astro Chase (USA)",
|
||||
"vid": "QmWnLvshgj6DBZLea8WRRyTqCNv7YXdSovTFERuCQnrm2p"
|
||||
},
|
||||
"1DFD9CB5EB608590C84E14376291F8C02FB1AB93": {
|
||||
"logo": "QmaLmD1rS78PgkjFGDcBNgq8ijUxnvVJ3bu1tb2fDmnXpn",
|
||||
"name": "Ballblazer (USA)",
|
||||
"vid": "QmcR5V87KAkwhwey3xaiG9p2JYuaXMvDDEobjknmenw6oh"
|
||||
},
|
||||
"A339CDDFC28477C21C58783D2D3B0CF0F4626DDF": {
|
||||
"logo": "QmdRwsaJGfSPwDdhdJ6XfDkQCb5Rep3i12Zt1Uo13rNEKM",
|
||||
"name": "Battlezone",
|
||||
"vid": "QmZ1vyBji3DLKU5ZF31txZfLApYP3wcE35gGP2trh6XKFR"
|
||||
},
|
||||
"B75D555D059AD61992F9F5A234129ECBF9504C35": {
|
||||
"logo": "QmXiPjkDS2K7MwJ1fDM2AWoXNdKpfkLUnETEWUiYQKE3bv",
|
||||
"name": "Beamrider (USA)",
|
||||
"vid": "QmXyKVbo5UiMjzYZLTqq29uaSG4ooSFrbDgxsgp2njVRdq"
|
||||
},
|
||||
"E56CCC6BF427A832CE00540B02D065A2688260DC": {
|
||||
"logo": "QmYnnuTWrzwQisvTizoP8KHg5aJ9yhGTTsx4wGwhLg34mK",
|
||||
"name": "BerZerk (USA)",
|
||||
"vid": "QmUsQPp9wiKET7oDpUvxVE8tsQff8JVgqJBuPrr861gbsm"
|
||||
},
|
||||
"1E1A20C104BF4D55ECA0CA1E72DC5DC64FA6B1B5": {
|
||||
"logo": "QmcsCBdtDATdz8A5CTFkJ4MvzPx34RoSuh44uXh9Ptr6xi",
|
||||
"name": "Blue Print (USA)",
|
||||
"vid": "QmT4bBdaASSHyVkgRwau3KeFRtdS3E824mJTqA6EmVMNXz"
|
||||
},
|
||||
"BC7C81F758DD3E224E42AC8A16C1A12D69AED722": {
|
||||
"logo": "QmbThDjtbpx22KZDE6mX3K6ttHG3HgaA4bJJoKVCFLxaTG",
|
||||
"name": "Buck Rogers - Planet of Zoom (USA)",
|
||||
"vid": "QmSL9HQHitQGqK8iK64CwUPCp1sNhPoyTXUHoTMd3W7yvS"
|
||||
},
|
||||
"C1A56A1FC51E09B1D8E93AF40DDD7596847E6164": {
|
||||
"logo": "QmXi2AbqLgjot7KEk2nFqaJNCe3CGX7gjxrFRtN6GQbw7i",
|
||||
"name": "Castle Blast (USA) (Unl)",
|
||||
"vid": "Qmf8omSFm43MtM7bkZUQgnJwGooGGSeuzGQSsKm9LcY8MN"
|
||||
},
|
||||
"4E022E101D7346A0A8618018372740E7333B4A36": {
|
||||
"logo": "QmUM15yrj5Bu67ZeyJJsTrJbu7ZGpbjnhWaEZHDZeiiGH1",
|
||||
"name": "Castle Crisis (USA) (Unl)",
|
||||
"vid": "QmVJNgSN5JsML5TGcgYnh3UqaUTSXeE1MYHP5VJ8VCFQJi"
|
||||
},
|
||||
"EB343B90D3DE696E69B1F039D4FB2C6AFB0D80C0": {
|
||||
"logo": "QmXUQmwGzuFdi5QpL5SNmxdemp8Wjd82dnizxQTgMxPxPb",
|
||||
"name": "Centipede (USA)",
|
||||
"vid": "QmPZ5sEE8fPwCwVWtWxypvczsjoKpqhi8UZ6PCzd5CLirV"
|
||||
},
|
||||
"9F1EE191319EF0261CC7074A81C0E6D6B7464CD2": {
|
||||
"logo": "QmdeFYnVUHGthGzpKBpQromfqbvQ4UWhfvUqhtRUCeSC6E",
|
||||
"name": "Choplifter! (USA)",
|
||||
"vid": "QmahcPZcQfLeq1spMGT2GyYxS1gWUivTMnH6Wm8zDApKJg"
|
||||
},
|
||||
"E0203C2124127EBA2D679BE548FA257ECC5B2607": {
|
||||
"logo": "QmWpfd6F8GED6g86YZpjEVQhZZZUgzzBRWoMzAEc7w66Cs",
|
||||
"name": "Congo Bongo (USA)",
|
||||
"vid": "QmYR83qSqmoe6b52vHuCLLQjBruMzN7oJhs8uKgkrhgScD"
|
||||
},
|
||||
"FA6D77E6E9AFD0EA42FC9F9807932BF9E8FB170E": {
|
||||
"logo": "QmaqFcdnztqSpjJ5Hp3ZXFfHujfpf2WFZu9vC7kpS97T2J",
|
||||
"name": "Countermeasure (USA)",
|
||||
"vid": "QmcmVTHavNERbjqhoKX6L17TsNYgnjfHBKekNz5hX5acvF"
|
||||
},
|
||||
"DB203973DF13B071DA46D52FDB078A76F4DB428D": {
|
||||
"logo": "QmYYExhk1ArSCNVigKF6DAAiEQsxBYrMREALDpfeuBEP9k",
|
||||
"name": "Decathlon (USA)",
|
||||
"vid": "QmPs9sSC9UFqCY7S2Zq6SDhsGVsGmsMjqdZo1JJvWrjTYx"
|
||||
},
|
||||
"44F6DBF5013A70E646D5C5232981D517069FC449": {
|
||||
"logo": "QmbBS3c5tRGGw6TegcQZrwiEBK3zEPR1Qwej6G2TexiZVm",
|
||||
"name": "Defender (USA)",
|
||||
"vid": "QmaT1ptxTbQes8Tu7C47oRCGQjwAABsGy5kEe8vx8BaRk6"
|
||||
},
|
||||
"387A76BA5912251E2285B7722004AD75E9EDE523": {
|
||||
"logo": "QmUmC5paAFvbzaL4ADJ4x7mfuhwUaLicxGjHnwhC6fNVqs",
|
||||
"name": "Dig Dug (USA)",
|
||||
"vid": "QmdnXdS2Hkva6thusggxswndoVtMX6oBFGFu2pgH48L29Z"
|
||||
},
|
||||
"EA7B40CE1106378E1D409CAB6634FB2E483FF238": {
|
||||
"logo": "QmRnZr6cVYDssaiyQg4dHMWeEfo14Uqr3hmDmjG6qFegDt",
|
||||
"name": "Dreadnaught Factor, The (USA)",
|
||||
"vid": "Qmc2KWinbeZtqwJthgyLPLeGoDxce9GRefuHrmfjVbuMgc"
|
||||
},
|
||||
"3D815594CF674694B04BB0FF90BF635D7D402FC2": {
|
||||
"logo": "QmXahWf2Q5H9nqmT9zvbDXATnMzERa4rDMeUgVjYBYciJ9",
|
||||
"name": "Frogger II - Threeedeep! (USA)",
|
||||
"vid": "QmashLCC6yAWu9Y2NUgiwGBYrmE3bm8vSVKXqZy6SCSmtp"
|
||||
},
|
||||
"D9A695757F0494AEE92A50AB8A49558745E0CEDF": {
|
||||
"logo": "QmfRb2WVd7yXBbywJJxMpAj8vYogVYgYBZa8FSGLTGfD4x",
|
||||
"name": "Frogger (USA)",
|
||||
"vid": "QmUkMmQALzoexfb1uH1pEhPP1gkNCy6JRrxn39zX28nGpy"
|
||||
},
|
||||
"0DF0D7F11D89E2C5B59CC9F98D67D98E203831AA": {
|
||||
"logo": "QmQTqXAHzDhUroESmQMuyLACw4YLDc7qrEyiJxyPPufjY2",
|
||||
"name": "Galaxian (USA)",
|
||||
"vid": "QmdKPFabRNmS3u9cesVTx8DhKYfD3i382RFUi73UYxtCbS"
|
||||
},
|
||||
"F3C32B39580D589799DCEAD98BB206FBF3DF0504": {
|
||||
"logo": "QmZnKTnvTsX6uh3kegW1ftpc5Zfw6stVq8EpbTigQJ42iA",
|
||||
"name": "Gremlins (USA)",
|
||||
"vid": "QmfJokqMPk28mKxhB5jfCEsV7dmKnF9KeLkAS3cepjbFMo"
|
||||
},
|
||||
"D0D8650EB07D1679004D491360C2C041E37C61CD": {
|
||||
"logo": "QmVgDXYMZNB6UtMBxyX2j4Lq2BVgsm4Qh8xjYQza3f7EjU",
|
||||
"name": "Gyruss (USA)",
|
||||
"vid": "QmWyPUhSYdczNUFEptr8NKFL957rhKgPUd5DfXxsdmGVgt"
|
||||
},
|
||||
"D286ECB6793A0DDA9C9C68F6355DBE3ADE6922F9": {
|
||||
"logo": "QmdJumA1Ng7XjdwM9ZtB1kBr6DEtR6ATznvKN4xnowxgjs",
|
||||
"name": "H.E.R.O. (USA)",
|
||||
"vid": "QmezV56L8QhPPYZi4n5oTq3DG5fhiZD82U8vZnT3BMp8GC"
|
||||
},
|
||||
"AE765943AE8D909B6CA6FF1560BA027ADEEAA515": {
|
||||
"logo": "QmXeo5oW1HirBWVvgb8jTXod87keHLWphTG29gNUwX8Bhh",
|
||||
"name": "James Bond 007 (USA)",
|
||||
"vid": "QmQH9YwezVS5JBFsvgYPV8KzMZqimunmo8oaLDHeZ6aut8"
|
||||
},
|
||||
"0F74125EDAC90FFD1274A128DF2970C239348FB5": {
|
||||
"logo": "QmWuSgH2U2NbG22F86bcDjQPW3QuLocsKtjcFWDWX1PrXW",
|
||||
"name": "Joust (USA)",
|
||||
"vid": "QmSwHpb7mzqDLzXMg43uVKTh86QrQMcueXSrLXZfscVZt1"
|
||||
},
|
||||
"FA02EE04CFAD90C6CCF9D9B984E3B7B3327FEC12": {
|
||||
"logo": "QmbVZ4N7n7UWz7AHU7DczSZ643c7wsikQLwz9XdtAwfMHF",
|
||||
"name": "Jungle Hunt (USA)",
|
||||
"vid": "QmWQroeUL3EZYkGi6SgJRhtNaRdSRB37sNiKNqorExtgHT"
|
||||
},
|
||||
"A3E760D09E4DC6053286967576B5B7D42EE2FD38": {
|
||||
"logo": "QmRqe5PY3wUMFHAL47ZQdoMwrwVgKGfv9GWyTMHBtC7faA",
|
||||
"name": "JuniorPacMan",
|
||||
"vid": "QmdmpG4aDxPSQ8G38AWxCW9JSb66DRaDGJRzzABzppfZY7"
|
||||
},
|
||||
"A80F876A89A8CEACC71DA5D9C02B546970B17730": {
|
||||
"logo": "QmR7DDdCKUmb51ZLfgDbPfHLk6AS4bYLQGbEnHjGXhBSmo",
|
||||
"name": "Kaboom! (USA)",
|
||||
"vid": "QmeBMT6czaD6VTXyPHGbndYciTh6JoZsn3SJCJpGdFDKc3"
|
||||
},
|
||||
"C3DBEED7ADC0E282BF76332AB5A3209550E26D02": {
|
||||
"logo": "QmdLoZBTM2FVMR4mXJMRajmZj6MzM1v8iAetFbED5obvo1",
|
||||
"name": "Kangaroo (USA)",
|
||||
"vid": "QmPZEobH9ykcMmdAWTGKrntLgykQjKwiWiukG2U8wMdJXj"
|
||||
},
|
||||
"9F31C85A13E919135438590601E33BEF11A8DF8A": {
|
||||
"logo": "QmbksYNgsB29NSMtyP5FJ1tktPjZdAqCoc5xdo2etx8vBa",
|
||||
"name": "Keystone Kapers (USA)",
|
||||
"vid": "QmdkWAKm7ixom612e27LoqZdR5h18bNgaZfLxjwgofRBCo"
|
||||
},
|
||||
"9547C56E2951E1B3A713B0B99D3EDB43C9D3CDD8": {
|
||||
"logo": "QmabvY8TyRDbEuCRFYKTu73P9UaFLrjdWHE6tc5XrneQoX",
|
||||
"name": "Koffi - Yellow Kopter (USA) (Unl)",
|
||||
"vid": "QmNYxQYQz7nNm2LxcR7mFcb12KFXZLdwvWBTGSMt4EzEjq"
|
||||
},
|
||||
"C7EB2B4A46A197ADFEDF33C2CCC17D8424D681C4": {
|
||||
"logo": "QmSoroGf1gZgSEJTv7b4x5gz4R7ZTuFstTbf5JG93ScMAd",
|
||||
"name": "K-Razy Shoot-Out (USA)",
|
||||
"vid": "QmaYe4g5vrTeSb7EkX6fizB5Mjfm8qMwkv2mJ1urEVPrsQ"
|
||||
},
|
||||
"A293B6B06050E11029D97137F2B1B36059567273": {
|
||||
"logo": "QmeKUDqJaispbQcSYmmZSwhbwpU7iaitT3VBhAZQm5dBKj",
|
||||
"name": "Mario Bros. (USA)",
|
||||
"vid": "QmWd5t6Q4b1ELMwLFmEqQpxVMXFnL8eH6kWcSvcynW6TUh"
|
||||
},
|
||||
"F5512460C6B6D3A1BAA0B23ADF6A3A37FE308EB3": {
|
||||
"logo": "QmS6Ax6RpdG5wLNq2p7c7JnrSUps6q9NJVNZJD6zvXxsiw",
|
||||
"name": "MegaMania (USA)",
|
||||
"vid": "QmV6sV7kXUPd5eGf4oo97nwRkomNHVK8BKDBFe2Fnj8rnC"
|
||||
},
|
||||
"32D86A381FEFF43149B66336DCC4BA2523273228": {
|
||||
"logo": "QmeBpap7TRpUwFRvfgb2YcgbdiEK4H3kvD4uju7oxkBu9z",
|
||||
"name": "Meteorites (USA)",
|
||||
"vid": "QmaxTgcSwkZSLY9mo6UMpz2Kxq2ck4vwMKg2AZUn7j3xWU"
|
||||
},
|
||||
"1243A8ECFC02A74AE4E1F4FCDA38502776F45CB0": {
|
||||
"logo": "QmdQ9j2WTo5gjt1TmuS5WEPmzZCnyFZnZVWQGpEiNpuuoT",
|
||||
"name": "Millipede (USA)",
|
||||
"vid": "QmS9LMkk4EPSJCTs72HGq5nzVjdjagw7oQLPHwUTGCs2Tz"
|
||||
},
|
||||
"0564B1867A0B570D66DFCBC11ADC3E51A2C6F28C": {
|
||||
"logo": "QmeiYfTs4NsivMGtVPCNBRzM5XEB2MMhTZjy8hmXHqvLva",
|
||||
"name": "Miner 2049er Starring Bounty Bob (USA)",
|
||||
"vid": "QmYnYpiQX5Au2bL6Y7Gu8EsnF6uUaz2RwHi68mFKq8ADcv"
|
||||
},
|
||||
"7E2A05625598C7E6615F969AA695E64315CD9846": {
|
||||
"logo": "Qmc2M6T5bN7dN65GGs5QwkvyPtJMgpN1WeeFMLYZMw6Hng",
|
||||
"name": "Montezuma's Revenge featuring Panama Joe (USA)",
|
||||
"vid": "QmPgzVcooQNG4bHxUEehkn9EAdQZszuCR3RhRKsHTXxsAE"
|
||||
},
|
||||
"02DD5B9278C06AE8CBB6DACC094E238FBACF08D2": {
|
||||
"logo": "QmbCgUbzYBHEC7UgwHyAMKgbbDPYtb3HPwzmS9Pubft9EQ",
|
||||
"name": "Moon Patrol (USA)",
|
||||
"vid": "QmUf8DgWXyL1eyQyHZdM1yvYzDHhep7Xs6G3YJgnvdKLkB"
|
||||
},
|
||||
"2E7AE57260624D1F5710D445A0936F0C280A6655": {
|
||||
"logo": "QmY8iAcgbkK8u93bs31Ufy3r1VHPMUjuWRUBtiC8pSefQ9",
|
||||
"name": "Mountain King (USA)",
|
||||
"vid": "QmW1pFdy7shVd3hJZiVTkWZGSBe7TTKwTDZrKczSe9Lgmr"
|
||||
},
|
||||
"002E6F8C7533B03F8041E7F1896589C0AB0E17A6": {
|
||||
"logo": "QmeXVArivdsCKhB6Fr4SYwvsKEPkBsxDqQYqxs3g6XZHoz",
|
||||
"name": "Mr. Do!'s Castle (USA)",
|
||||
"vid": "QmTzFpQEGbSCtB1Noxn8CjZvusaS1uN3ms8P19t3ReL7tA"
|
||||
},
|
||||
"F93D72444D76075273E508555423936530875148": {
|
||||
"logo": "QmRsmAGf4MnHd9HyCRw44uq869ZySzMkZTB1KTpp4Z9Y1K",
|
||||
"name": "Ms. Pac-Man (USA)",
|
||||
"vid": "QmaZsa9y9HnvKkXoh9TiDiB5qVBCM6WYqDb13kUYNHykYy"
|
||||
},
|
||||
"EE4B3C6F08AA9FDFAB532EEC98136306B6923F75": {
|
||||
"logo": "QmVGMiNFNA5BXihh62Royzo5NmWiUX4bnnCmgrq9paq7rJ",
|
||||
"name": "Pac-Man (USA)",
|
||||
"vid": "Qmd9FrnaZxie1kjaCwcrVEQwFYYV8rJ8Y1wtEUMNN1jgV5"
|
||||
},
|
||||
"B051F0FB2E816787AB6EED0CB008B9F8CC9963DC": {
|
||||
"logo": "QmTHKZHB2DiJYxjayL1Udo3oF6zgazmBMgAfexxMb1Fwpv",
|
||||
"name": "Pitfall II - Lost Caverns (USA)",
|
||||
"vid": "QmWvq7C2tpimgY8yTEoTVwbUBrMSaU5micsJazYMsB3Zwn"
|
||||
},
|
||||
"D1C7DC3DE097A4BFAB82235ACEC69A97E6E05F18": {
|
||||
"logo": "QmT6der1wYz5G9wWvC8BD76417RXt4zmzz3Sjk6dhyLR81",
|
||||
"name": "Pitfall! (USA)",
|
||||
"vid": "QmWnriRFBrDE2YhKUYKam31ohiU3TT6k1XBb7BZrggHtLa"
|
||||
},
|
||||
"D504764C5894EE99C0B5E2024CBE833EBBA3AB17": {
|
||||
"logo": "QmeFSsfRnJrCo85pJxUzWLmKwLqP456QXMCUkA5Vog7eav",
|
||||
"name": "Pole Position (USA)",
|
||||
"vid": "QmdH7VbvPSh6kudeZNMt8emHSNiiu4EABfeMUUXu3Gx94S"
|
||||
},
|
||||
"E615AAA0C1D56AC83B91F6087970FA708D6433BF": {
|
||||
"logo": "QmQEQAvhKGK9oSw7TQxvhqLDfaLgyiMAdB99RYe9XirDdy",
|
||||
"name": "Popeye (USA)",
|
||||
"vid": "QmeQJeRMbFyLMrS8kkxNvB3Ts4woTtNmYVoZQDytnRZXTk"
|
||||
},
|
||||
"E6966BDE5CD4167020C9D21BAB613AF1648B4D75": {
|
||||
"logo": "QmV2U7z5Xvc92keWQss7bEJavf22G153ENhTiVM8DFrkth",
|
||||
"name": "Q-bert (USA)",
|
||||
"vid": "QmP3LK8fTjyG9ToJZugmwm7kicr8uHEFdyw1RiiqwqqrmX"
|
||||
},
|
||||
"786CDF54D1625E0D3EA8814506FDEEBA5E4ECE48": {
|
||||
"logo": "QmcTdE9fG2W42AWWGanSDueyzEbFoDZADZtH8dDBt1DFR1",
|
||||
"name": "Qix (USA)",
|
||||
"vid": "QmedGY8aE5Do1N91CZw2Ki8FhTiMTq8qs8cyC5aocH4WV2"
|
||||
},
|
||||
"A94D06739FA19453FA586A836EAACAE11FE93989": {
|
||||
"logo": "QmNTBNDoGMjxk7ZBe3thDhcZHPfr1K6LUK3Cv7mmq1Rdon",
|
||||
"name": "RealSports Baseball (USA)",
|
||||
"vid": "QmVi7v5sHztkdZN5exui5LpWhRXqm1hMkNHwicjNHhP3zw"
|
||||
},
|
||||
"A0DBD654ED3BA2583CA175A1356924D9529884DF": {
|
||||
"logo": "QmUgfKdaMbzbrQ47ByYrpkrXKsASyaiydXonYTAEynMUMp",
|
||||
"name": "RealSports Football (USA)",
|
||||
"vid": "QmQz26rhmE1UxrxwqwyEZm6Q75amkVjHitUvVQXkTtrk8o"
|
||||
},
|
||||
"C911CC914F3923D5AEB1A897E6DC4A551A9A53DA": {
|
||||
"logo": "QmWRmftRZSt9HpFQuFrKgxbQXeCyyqxV1fgwCaWBWvZDHM",
|
||||
"name": "RealSports Soccer (USA)",
|
||||
"vid": "QmNq69X2m7xWogqkqcThaGzEDhWEc6HRBNAZDPZxFMkxmP"
|
||||
},
|
||||
"9253D7286CF87999474B37DF242BA23B4358EF4A": {
|
||||
"logo": "QmdggyhfUoj3kDxyykeyPQLHhKo8iAPK8qqGTfybKg6Y8N",
|
||||
"name": "Rescue on Fractalus! (USA)",
|
||||
"vid": "QmdAhDGegbXeyfGPhxUgx29TFHHxDWPDBwdCFcDcUzwbkV"
|
||||
},
|
||||
"CE9BD3B0847C5E187C3E8F667C8F5EA771C49965": {
|
||||
"logo": "QmTMtgzQzdkRbucQ2UnrXLN3cnQzA9y3BcWcy85xN3Ddef",
|
||||
"name": "River Raid (USA)",
|
||||
"vid": "QmdiQJGpsQXesH6hSF1iUMcbVEPYj1gNAMP97ypH8Jg36E"
|
||||
},
|
||||
"9175E6DACA58102AC8B9729D7B0336CF09977D12": {
|
||||
"logo": "Qma6EtHyQw89y6AEGTTd9oTTjfvY6JjcmfvyT5CFwAET1c",
|
||||
"name": "Robotron 2084 (USA)",
|
||||
"vid": "QmW5W8tfaE8zVGB5Dy7XyMVDd6FcbRjoYP8aHLFT561y8j"
|
||||
},
|
||||
"127ED1C31C3E85D10212EEE7B9E6D71AEF7DD891": {
|
||||
"logo": "QmVFirQhANh8PAwebsPg4p622eQTUr8SUoq5tijWu88Dux",
|
||||
"name": "Space Dungeon (USA)",
|
||||
"vid": "QmQZPvKr4BWSwEKs3bNGPG3ARBZiM3uQNvgXS4x3F3jb3f"
|
||||
},
|
||||
"CF411DB770217B5AF5FCFA32725CC587A8B9E710": {
|
||||
"logo": "QmbvpjyfFAdh78FncJXi4yiR5uP5r2EAjZZUsgUixbij5k",
|
||||
"name": "Space Invaders (USA)",
|
||||
"vid": "QmY86x5hrs7vCpJ2roHc6QuBiqDVLU1N8KJLr7zfGRUh4i"
|
||||
},
|
||||
"5AE20E767D9819FE601F3B1EA6D4506AE2F2CEEF": {
|
||||
"logo": "QmSyML8DYf5FKoartTHUoGSaUcftHdx3LVwmspFYrTnm7q",
|
||||
"name": "Space Shuttle - A Journey Into Space (USA)",
|
||||
"vid": "QmcWizEUkRraMAvciPpsVWrUzpYq1Y8B7KfHCGJf8r13WQ"
|
||||
},
|
||||
"09D192A3598CDADFB61ECEB430B3D5892E40962F": {
|
||||
"logo": "QmWte466CT5962MQqx9TbG4qKxAreP1zvwUsk7dB1jS7FR",
|
||||
"name": "Stargate (USA)",
|
||||
"vid": "QmPFnrGz7FQUootHdY5ZbLkz2hQ7tur2JUTqmfHoeaHS6r"
|
||||
},
|
||||
"57DFC22C9805555F957ED51A2DC7BF35AD00C9D9": {
|
||||
"logo": "QmWVN7QL5yz4GXAjGJ15YUXtMxpVUmYVGi2ceGbfnZrdZD",
|
||||
"name": "Star Raiders (USA)",
|
||||
"vid": "QmSr6WuMCx5X52h8c2kmeNJg5fWJdTSHeuVu7QxK327vHD"
|
||||
},
|
||||
"19801454AB86F2A5CF691FCD577A71AD2B8DC7D5": {
|
||||
"logo": "QmQuH8XDbmMTag4a6JMNMHtxan6itTidDdFm2K9bUc8Ye7",
|
||||
"name": "Star Trek - Strategic Operations Simulator (USA)",
|
||||
"vid": "QmUzzCnJkWb5vdV1FY3k3sUQD3u63joTYR4XVDe6zda32y"
|
||||
},
|
||||
"BF77048ABC36BA2CBA5EEEAC812802D6A0EBA891": {
|
||||
"logo": "QmegpAeew8eytGwBtYgArwx1CqFYS1buxafQcs9yAiUGYE",
|
||||
"name": "Star Wars - Return of the Jedi - Death Star Battle (USA)",
|
||||
"vid": "QmTWG7Hx8fvdWixRaLKhpMhq5VWzjnzWF5b1JqN1W8C4FU"
|
||||
},
|
||||
"AB676FFE054B23BDD43CEE2498AFEFAABBF9355B": {
|
||||
"logo": "QmduE8bZMQ1DLUn7VVT8MRpyZ31wBKS2NVZciH7J5j3zBd",
|
||||
"name": "Star Wars - The Arcade Game (USA)",
|
||||
"vid": "QmaDuPMh32BjZgJN6VsGca9n7jaoUXvF1tWiHpa78VkaWd"
|
||||
},
|
||||
"8205093F5FB1FDF0CCB71D667EA0A3849FB0A0F2": {
|
||||
"logo": "QmdiTUqMwbuCiSpBT4XBGyVfF5HuGpdTtrMLrgYV1dwqxD",
|
||||
"name": "Super Cobra (USA)",
|
||||
"vid": "Qmb62ZEBSQhcCGjtqgtKSKH6tRhNbqZeNdQNsHk7SFwe58"
|
||||
},
|
||||
"238CB448555D8BB5C96AA0354718411E8777C613": {
|
||||
"logo": "QmNUacpZwsrCNQc2MrDBj2YNyqKrq9gVoWykm5CMRm2Du3",
|
||||
"name": "Super Pac-Man (USA)",
|
||||
"vid": "Qme6UNBn1ZK1cW7scsSfBeQNcvEgRQ4A9myix6jThR5MMg"
|
||||
},
|
||||
"4046F60602254A7D86D2A6EFD1A330455EAEC119": {
|
||||
"logo": "Qmbndnwr65MpH2ZKH7gYgXjihYKLSuQUfaJpuA1DUnPUjP",
|
||||
"name": "The Last Starfighter (USA)",
|
||||
"vid": "QmaG6SgHsE1ZSYjTgtK5zfoL1CzVd2rYtNfXAgTL5w55H1"
|
||||
},
|
||||
"6C633DE50E6CC0C0C40061CD24E6F78F552A438F": {
|
||||
"logo": "QmRnpaubP1ek7Cemrw9v6wGHnED29CtMaQQrtmmp7yRHSq",
|
||||
"name": "Vanguard (USA)",
|
||||
"vid": "QmSAtWhrDVu9W21Ru9iUQW4ftksTRX63NSUGthCRHyJjkv"
|
||||
},
|
||||
"49DEBDF3C133FDE9905D6D9137DA5D8DFF8D6283": {
|
||||
"logo": "QmdR2fqAUZAmBZMpM3DdquW463kzMhqVwPbCCS1RyWLk4U",
|
||||
"name": "Wizard of Wor (USA)",
|
||||
"vid": "Qme2stUTVaQix1sTDVujXt7vjYLbKWPiRprSjaFFFnMKhw"
|
||||
},
|
||||
"C3CF33216DFA484309B923F21829D040486BC481": {
|
||||
"logo": "QmSEYo3vsjWY7QDEe84CPzRrtEdFBp6VZd4nQizXhv4CRn",
|
||||
"name": "Xevious",
|
||||
"vid": "QmP4nVrr7pQuo2Ao483fPhpTeu2wKHkCV5fPMmC1XkqyRn"
|
||||
},
|
||||
"4A4C4A25C8ED1B0AE79CA22684D7D311A564A7B5": {
|
||||
"logo": "Qmc18GGCAdh1S3h5hhFtjxBeArTdNU7iwPdWDu1574APLe",
|
||||
"name": "Zaxxon (USA)",
|
||||
"vid": "QmQuxstBK3mjjoNKnyEYoxtphgFR2Xt3CybvzLb82Uyfws"
|
||||
},
|
||||
"55DA563B4394508C23D866FB1E543262FB45536D": {
|
||||
"logo": "QmRfqMXY7gdGB6mzNrr8XGRWRHXKbWAJxtQ1ZmUW2qoVSs",
|
||||
"name": "Zenji (USA)",
|
||||
"vid": "QmRQrC3tXVi9KkEnLEiqCq8pYSdHr68X7oDLDs2xwTmktG"
|
||||
},
|
||||
"18315BEC1E62102290FEEB77FB290E1266CF5BFC": {
|
||||
"logo": "QmVzAYCnfJAXXVJsvqvcxrvK75ZHkr4zvoGW8rQJitnSvr",
|
||||
"name": "Zone Ranger (USA)",
|
||||
"vid": "QmX6eHZqHk2adF7dz9afyedH3FioHwvw77r3pAdmt1YGfP"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"file":"/data/main/videos/nds.mp4",
|
||||
"cid":"QmfP1qiC1ej9dCeX1dBNuRyMXhQTDsHKwiYi2cBpJxmvf6"
|
||||
"cid":"QmXX9BebDhPr2oMuv5L9SizTwsbhvR9J3sYPMSKaRAu11Y"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/videos/psx.mp4",
|
||||
@ -119,6 +119,10 @@
|
||||
"file":"/data/main/videos/atari2600.mp4",
|
||||
"cid":"QmXR4JG2XMjUu5qwBkR6HBR8fC8MkqL1cCZDFJGpcMuENv"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/videos/atari5200.mp4",
|
||||
"cid":"QmedGY8aE5Do1N91CZw2Ki8FhTiMTq8qs8cyC5aocH4WV2"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/backgrounds/psx.png",
|
||||
"cid":"QmXtrPJCdPuBPemYLxC8shQYHNHk59ES26FyGqR12bZiD2"
|
||||
@ -179,6 +183,10 @@
|
||||
"file":"/data/main/backgrounds/atari2600.png",
|
||||
"cid":"QmSXacVTbftY1vsoK9RDJ47Jz2yeJB18td3rtTPmpfTw7D"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/backgrounds/atari5200.png",
|
||||
"cid":"QmNoDmFBcrE2wDHMVaZxCvRQsua1UjuYSfnxGshKY8YLKA"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/backgrounds/segaCD.png",
|
||||
"cid":"Qme8N7PzVPstNCQ3V5PpRmmBzokwRsHPjJQJgDbjBxnpmR"
|
||||
@ -217,7 +225,7 @@
|
||||
},
|
||||
{
|
||||
"file":"/data/main/backgrounds/nds.png",
|
||||
"cid":"QmRi2JqtZojLMAfcjgZjDBuTJzMoosJoDz3fcmaG4StyJi"
|
||||
"cid":"QmeEfewgssCsPhzzfc5ZG4hVx8xgsvEKof7yc8rgKE6e8u"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/backgrounds/pce.png",
|
||||
@ -303,6 +311,10 @@
|
||||
"file":"/data/main/logos/atari2600.png",
|
||||
"cid":"QmTKD7vsGBwTkRgaxV7mFjgxHrYrkiAnLTjNXs4HV9kTQV"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/logos/atari5200.png",
|
||||
"cid":"QmQZRWCPp2wRVztiWgcAKNKFuRXBTNCpiaeSxw7LEhS2bc"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/logos/segaCD.png",
|
||||
"cid":"QmV1b4fCtBo4iRqZDLTYctWE9PdKqVRqi3ZfgjffTKzCsD"
|
||||
@ -427,6 +439,10 @@
|
||||
"file":"/data/main/corners/atari2600.png",
|
||||
"cid":"QmarPeRQzXTGPY88h744vhgCu1RGJtpV9HyTtjhrQmnQhq"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/corners/atari5200.png",
|
||||
"cid":"QmfTa42eaceCXkdg9NDkAziPNAfKUfAFdftrbFt2XYrkaU"
|
||||
},
|
||||
{
|
||||
"file":"/data/main/corners/segaCD.png",
|
||||
"cid":"Qmc39zPV8PwrZTnMHNqk4mPXZpLkhkTAgkaF2B5f7WKhiq"
|
||||
@ -495,10 +511,6 @@
|
||||
"file":"/data/segaCD/backgrounds/default.png",
|
||||
"cid":"Qme8N7PzVPstNCQ3V5PpRmmBzokwRsHPjJQJgDbjBxnpmR"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaCD/bios/bios_CD_U.bin",
|
||||
"cid":"QmR3LZ4wsd8HAEfpYrWHt4KSKSwkcLt243dETYCr9VwRAo"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaCD/corners/default.png",
|
||||
"cid":"Qmc39zPV8PwrZTnMHNqk4mPXZpLkhkTAgkaF2B5f7WKhiq"
|
||||
@ -523,17 +535,13 @@
|
||||
"file":"/data/gb/backgrounds/default.png",
|
||||
"cid":"QmWcgbLd6rKAUGp1Q2DEfEQf1t8p8g2HN1XaHL8ZPVGWPU"
|
||||
},
|
||||
{
|
||||
"file":"/data/gb/bios/dmg_boot.bin",
|
||||
"cid":"QmXzNc3D578bHUgLwD5mAkQHzAVhmsbL1jqhfJqJ5AEN9T"
|
||||
},
|
||||
{
|
||||
"file":"/data/gb/corners/default.png",
|
||||
"cid":"QmQuTP338HcMh9QLEXDZdVLKbf6YCQV7dmx1UcPsdiL5DX"
|
||||
},
|
||||
{
|
||||
"file":"/data/nds/backgrounds/default.png",
|
||||
"cid":"QmRi2JqtZojLMAfcjgZjDBuTJzMoosJoDz3fcmaG4StyJi"
|
||||
"cid":"QmeEfewgssCsPhzzfc5ZG4hVx8xgsvEKof7yc8rgKE6e8u"
|
||||
},
|
||||
{
|
||||
"file":"/data/nds/corners/default.png",
|
||||
@ -543,10 +551,6 @@
|
||||
"file":"/data/gba/backgrounds/default.png",
|
||||
"cid":"QmQWF2Dr3QnwJhzaoptJUF2XzCHJzwoiKxuReDshBDr6Yf"
|
||||
},
|
||||
{
|
||||
"file":"/data/gba/bios/gba_bios.bin",
|
||||
"cid":"QmVWJiqWxzExiLq7T2j9ADp3H3rDoNnAkAr1sttcsw8Utq"
|
||||
},
|
||||
{
|
||||
"file":"/data/gba/corners/default.png",
|
||||
"cid":"QmQuTP338HcMh9QLEXDZdVLKbf6YCQV7dmx1UcPsdiL5DX"
|
||||
@ -571,10 +575,6 @@
|
||||
"file":"/data/segaGG/backgrounds/default.png",
|
||||
"cid":"QmV8LcGgjpqsXGqMewWhJVCbaSJn5QfzitcSmtPy974HaZ"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaGG/bios/bios.gg",
|
||||
"cid":"QmcM47ctYJG2ijtuWA5QcjsKXonoGKTiSKQ5j9uzRVUueP"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaGG/corners/default.png",
|
||||
"cid":"QmQuTP338HcMh9QLEXDZdVLKbf6YCQV7dmx1UcPsdiL5DX"
|
||||
@ -607,10 +607,6 @@
|
||||
"file":"/data/pce/backgrounds/default.png",
|
||||
"cid":"QmXVxvCqNDhTYpEvweW6hGDNG5ojpsLhsGF4MEChsma2JW"
|
||||
},
|
||||
{
|
||||
"file":"/data/pce/bios/syscard3.pce",
|
||||
"cid":"QmU5f43aAAQtAtrDVqcq5BWz95HSNa4wvqJwgpjbK5xVTx"
|
||||
},
|
||||
{
|
||||
"file":"/data/pce/corners/default.png",
|
||||
"cid":"QmeVdvxJt2tcUWQJubJamt4MSuq4A1SfTvSz6F3J5XzhMo"
|
||||
@ -625,19 +621,31 @@
|
||||
},
|
||||
{
|
||||
"file":"/data/config/pce.json",
|
||||
"cid":"QmTuXqpMqwKD4LbsnksAeTwQmaMhrkkbXvrCSUYAUYWQG8"
|
||||
"cid":"QmNmCQQAae4QG6JxNvzqfMP2XgsF8w6gDzeXiyKKz7JwTo"
|
||||
},
|
||||
{
|
||||
"file":"/data/pce/bios/pce.zip",
|
||||
"cid":"QmVZuSTmjWBbFvsgkV7fp4R1ZmjveK9Fap4Nxi6DhbpZPT"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/gbc.json",
|
||||
"cid":"QmVELYAWho5xuwSo8hpZbiH8BoHTBxMS5pDSBtVpwYheLY"
|
||||
"cid":"QmYb7Qs81M6v2AR9Fe1i2hS4ff1K8NWdBe3ZEHBoxFUL44"
|
||||
},
|
||||
{
|
||||
"file":"/data/gbc/bios/gb.zip",
|
||||
"cid":"QmYXjcLTGdtG9JtDGVg42cWFkW6feYufRPQYGSe6ifESY8"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/lynx.json",
|
||||
"cid":"QmQrZSx1W6qKjdXmCJFX26Kg8ZN9XtahFgNaRyacs4kbzb"
|
||||
"cid":"QmYsDZRt9vk4WPgMpbDwqtXWZmMvxyJTPVFsSA7YvBBYMd"
|
||||
},
|
||||
{
|
||||
"file":"/data/lynx/bios/lynxboot.img",
|
||||
"cid":"QmTaUUheMwLj5JqTqiZLzUfdUBj9xWikyZY4s72RGfKb9d"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/segaCD.json",
|
||||
"cid":"QmddqqPJ8m92LvaiM5RaPhPrj43khMavb58QVgfnC4DrV8"
|
||||
"cid":"QmZDGK5aFqMoTpzZJz9V8S5P6pxuqDP3RwDWYrGpiBMhdY"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/vb.json",
|
||||
@ -645,11 +653,15 @@
|
||||
},
|
||||
{
|
||||
"file":"/data/config/doom.json",
|
||||
"cid":"QmdbtjxaChjJqanfFarHJw7nYwxdsnSgYRCaKKtGkzyguX"
|
||||
"cid":"QmXc3LLZkTnvz6GSftb8LycevPyFnHGwyBMayyyD1AuV5J"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/gba.json",
|
||||
"cid":"QmX3ETMRooA2YSJoHfVyThTFE1bWiYCA9Gog8dKziRJbu1"
|
||||
"cid":"QmSdiXmb2538xy4jSvy2Uy74AxyZvcra4jXYZWY9UQCQrt"
|
||||
},
|
||||
{
|
||||
"file":"/data/gba/bios/gba.zip",
|
||||
"cid":"QmYtDaErc2RzWBYLmcwfKD81NSVjqBsGD6cJCk3kNzDyuk"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/vectrex.json",
|
||||
@ -665,15 +677,19 @@
|
||||
},
|
||||
{
|
||||
"file":"/data/config/main.json",
|
||||
"cid":"QmYUtztFTLXbgyu9KVcmtVtsgqTw7ty26ju2GPwVfwCkSX"
|
||||
"cid":"QmfTg6rcFprn1jQVaX6CJFE3f73sshVwVyjyg37JZ4cLPM"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/segaGG.json",
|
||||
"cid":"QmPfQUHixS1pRoKUDdgawFSMsFKpjt46DFyW9P9fbCVHKF"
|
||||
"cid":"QmZjX8naFHrD16kXasyJBZGwB1xvzjUjN5J9xsqjcgeLpA"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/odyssey2.json",
|
||||
"cid":"QmXmFBV2RdXP2ukA6r1AeuqvTVvHTdYntQF4jtjwUonuvP"
|
||||
"cid":"QmQuJmH6vhymWQp1oWAaPjL9MSx9k2LgVFGLq9iH4rN2EA"
|
||||
},
|
||||
{
|
||||
"file":"/data/odyssey2/bios/odyssey2.zip",
|
||||
"cid":"QmcXRfDzhsEnWnMykpVwQc86kZXKXgbWMKuXf3HNzXnWKN"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/colecovision.json",
|
||||
@ -697,35 +713,55 @@
|
||||
},
|
||||
{
|
||||
"file":"/data/config/segaSG.json",
|
||||
"cid":"QmboVi6qLgZ9YSj4CnkPgJg5c6wnS88SCB7wiagSPA7CaR"
|
||||
"cid":"QmXD3EUaQbv7jUMSTaAY7ViCnDAwu53qpFMJeLdvdn6jpb"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/nes.json",
|
||||
"cid":"QmTYx6yK6zB5fpC6Z9xswge42JS8TMzfpad3rXhYNCFYV4"
|
||||
"cid":"Qmb94JynyJ5FfLxfpbhCp4vLxmJWarKbesiNa7M81u8i5w"
|
||||
},
|
||||
{
|
||||
"file":"/data/nes/bios/nes.zip",
|
||||
"cid":"QmPk9EKQ2aRVHJX91PTJfmfBDiHRzeWuZExT36BAk2sNZL"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/segaMS.json",
|
||||
"cid":"QmXzi2gw4oTtFEqBH8JhuLXDVAzKmN3PGfTCDhkWKBaDqQ"
|
||||
"cid":"QmVrZCSBsWdztUtmPrALKZPfTcJk2NUDNszKRqZko4SoH5"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/gb.json",
|
||||
"cid":"QmScD8DusjpqSuCKT42bqe9v1uH5p4xxEYBnAYeMhtrHzA"
|
||||
"cid":"QmZyDRXQdoFg8izaPURzEQ8Zv4trVVk2G5jspCng7ruaVZ"
|
||||
},
|
||||
{
|
||||
"file":"/data/gb/bios/gb.zip",
|
||||
"cid":"QmYXjcLTGdtG9JtDGVg42cWFkW6feYufRPQYGSe6ifESY8"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/arcade.json",
|
||||
"cid":"QmUXaDzeCediDpefawxTvVFdrZs2PJ3vgsUoUpQkeLYrNd"
|
||||
"cid":"QmTvpfgqX9in7kGyuKPSrjjwJeRzZ4fHxZas7bARUUZ7MM"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/snes.json",
|
||||
"cid":"QmW2bp9HCL6ZbheYVwkCbczGwo2jEuuz6VEqMJ72bzzpLN"
|
||||
"cid":"QmPTSsowVAeMTUh8b31S1xb6uAmJVJq4TM6qYAKPrmNbqa"
|
||||
},
|
||||
{
|
||||
"file":"/data/snes/bios/snes.zip",
|
||||
"cid":"Qme4WxcE38pAZePr3jbHp4h3c9Bu23K7Gt7PNkVHU4x14e"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/atari2600.json",
|
||||
"cid":"Qma9uNpoCSNGfrxF8KxhZGCjqmvnXqk6n4DDHwW69WR2ui"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/atari5200.json",
|
||||
"cid":"Qmb5Adw5BUopBCtGxu9X1J4XS28n6aMMeTPUZpVAxb4nQc"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari5200/bios/5200.rom",
|
||||
"cid":"QmbDoTr3Vu3B4vqRkH75kiFHk7ChSJmzS367mc1qxVkw5V"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/segaMD.json",
|
||||
"cid":"QmSzmETrvkLfFgr3bPbwEm8tBhY6deuzQiUQZmCLdPb93F"
|
||||
"cid":"QmUNbgxrPrT5E8dZ8hzjMSLyRgUY4RDJVSVozwmkNjBsKB"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/msx.json",
|
||||
@ -733,15 +769,19 @@
|
||||
},
|
||||
{
|
||||
"file":"/data/config/atari7800.json",
|
||||
"cid":"QmZDZ5ENbes4ATmt4yXyPoTjaKKT1FDyyMUaaeEVuHX3vX"
|
||||
"cid":"QmSLHkLrMNwwDTaz9gTVVWKGQZRwTx2VrzuUVg5dDAxtNe"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari7800/bios/atari7800.zip",
|
||||
"cid":"QmcHsJd8dzc4gKwd3cmibFBAgBw3X7pswzdLsWSBG8Ngs7"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/n64.json",
|
||||
"cid":"QmePgQExUzCZCDx5sD1kdMDQaEoCQRj5BUjhxLUGDN4TUD"
|
||||
"cid":"Qmd7dS5VeSTfkd5R5aZvWx8vYfWSVfayFf71uWTooMpzFv"
|
||||
},
|
||||
{
|
||||
"file":"/data/config/nds.json",
|
||||
"cid":"QmYR6qJXgPtzy8322HMJP5uU4UvFnxxWdWBKhpQCcpJaJ1"
|
||||
"cid":"Qmbhz89RLAYiux8YjqXwqXxHwxtTghFnd5RERUJohphYsY"
|
||||
},
|
||||
{
|
||||
"file":"/data/nds/bios/nds.zip",
|
||||
@ -771,10 +811,6 @@
|
||||
"file":"/data/gbc/backgrounds/default.png",
|
||||
"cid":"QmfCkpZzcRaJYuCxuNBWtjVA28T9M25hNWQeqUQYZbZij9"
|
||||
},
|
||||
{
|
||||
"file":"/data/gbc/bios/gbc_bios.bin",
|
||||
"cid":"QmNQJie3w8pXAjXWWXRdJrsw8JfF6AZ2UwN4MQtDyPrz6U"
|
||||
},
|
||||
{
|
||||
"file":"/data/gbc/corners/default.png",
|
||||
"cid":"QmQuTP338HcMh9QLEXDZdVLKbf6YCQV7dmx1UcPsdiL5DX"
|
||||
@ -783,10 +819,6 @@
|
||||
"file":"/data/psx/backgrounds/default.png",
|
||||
"cid":"QmXtrPJCdPuBPemYLxC8shQYHNHk59ES26FyGqR12bZiD2"
|
||||
},
|
||||
{
|
||||
"file":"/data/psx/bios/scph5501.bin",
|
||||
"cid":"QmVqWZXiKdhcTFYnwU8iCYnPSBwAijsQx7c2kBDixRv9bL"
|
||||
},
|
||||
{
|
||||
"file":"/data/psx/bios/psx.zip",
|
||||
"cid":"QmfQTsRzaE4T9LUWnKara8KVyXmoUn2RXzmSHYtDYN8Jr6"
|
||||
@ -815,10 +847,6 @@
|
||||
"file":"/data/segaSaturn/backgrounds/default.png",
|
||||
"cid":"QmQg9KY4puPcxj1KpdfV49uq6z8PcEk8Fbq7uTdj8aYQSx"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaSaturn/bios/saturn_bios.bin",
|
||||
"cid":"QmPAPRxLu2vwr1LDX6JFa2kmC1LaufZVV2WF67emaf6hAJ"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaSaturn/corners/default.png",
|
||||
"cid":"Qmdynxj7tJmnLcMCJz1gYafXLwAY4jHHyYyqFPXzE7To7P"
|
||||
@ -843,10 +871,18 @@
|
||||
"file":"/data/atari2600/backgrounds/default.png",
|
||||
"cid":"QmSXacVTbftY1vsoK9RDJ47Jz2yeJB18td3rtTPmpfTw7D"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari5200/backgrounds/default.png",
|
||||
"cid":"QmNoDmFBcrE2wDHMVaZxCvRQsua1UjuYSfnxGshKY8YLKA"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari2600/corners/default.png",
|
||||
"cid":"QmarPeRQzXTGPY88h744vhgCu1RGJtpV9HyTtjhrQmnQhq"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari5200/corners/default.png",
|
||||
"cid":"QmfTa42eaceCXkdg9NDkAziPNAfKUfAFdftrbFt2XYrkaU"
|
||||
},
|
||||
{
|
||||
"file":"/data/lynx/backgrounds/default.png",
|
||||
"cid":"QmXnPKdmdsPKfmcvh63xwE5A19iMMAFz2HXHGBgJ7wANmP"
|
||||
@ -867,10 +903,6 @@
|
||||
"file":"/data/odyssey2/backgrounds/default.png",
|
||||
"cid":"QmXxgaCQQ5SonNQJ5ajiNDnk7Zf8CStFL8HRodY9mGRsQJ"
|
||||
},
|
||||
{
|
||||
"file":"/data/odyssey2/bios/o2rom.bin",
|
||||
"cid":"QmY8LpCqtwRzYBFUrkADcTTwJHwD9Q2oFPakxw1cpS1WJz"
|
||||
},
|
||||
{
|
||||
"file":"/data/odyssey2/corners/default.png",
|
||||
"cid":"QmXHdF3vrctFfovgndZfLXEKS5sjRTSinjPK7LmJwSM9Y5"
|
||||
@ -919,6 +951,30 @@
|
||||
"file":"/data/doom/corners/default.png",
|
||||
"cid":"QmQuTP338HcMh9QLEXDZdVLKbf6YCQV7dmx1UcPsdiL5DX"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaCD/bios/sega.zip",
|
||||
"cid":"Qmct6NWgAbSvWW9XenBfJ8U15GiwEgDLNFKhCnhL6TFiad"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaGG/bios/sega.zip",
|
||||
"cid":"Qmct6NWgAbSvWW9XenBfJ8U15GiwEgDLNFKhCnhL6TFiad"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaMD/bios/sega.zip",
|
||||
"cid":"Qmct6NWgAbSvWW9XenBfJ8U15GiwEgDLNFKhCnhL6TFiad"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaMS/bios/sega.zip",
|
||||
"cid":"Qmct6NWgAbSvWW9XenBfJ8U15GiwEgDLNFKhCnhL6TFiad"
|
||||
},
|
||||
{
|
||||
"file":"/data/segaSG/bios/sega.zip",
|
||||
"cid":"Qmct6NWgAbSvWW9XenBfJ8U15GiwEgDLNFKhCnhL6TFiad"
|
||||
},
|
||||
{
|
||||
"file":"/data/arcade/bios/samples.zip",
|
||||
"cid":"QmNMsqvdXobsGYmQ7nWEF63BEWYjQER4d9H3cLFZWpnLtm"
|
||||
},
|
||||
{
|
||||
"file":"/data/3do/roms/",
|
||||
"cid":"directory"
|
||||
@ -931,6 +987,10 @@
|
||||
"file":"/data/atari2600/roms/",
|
||||
"cid":"directory"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari5200/roms/",
|
||||
"cid":"directory"
|
||||
},
|
||||
{
|
||||
"file":"/data/atari7800/roms/",
|
||||
"cid":"directory"
|
||||
|
||||
@ -1,24 +1,28 @@
|
||||
{
|
||||
"C6612AC5A8AC2E2A1D707F9B2869AF820EFB7C50": {
|
||||
"2C8212631B37F21AD06D18B5638C733A75E179FF ": {
|
||||
"logo": "QmSNo8VrwLg1NRWAMhm2qc3zWFF7HK8563sAeU4Hu6DQ8u",
|
||||
"name": "Doom",
|
||||
"vid": "QmTuyvkgZ7H4kxLN5ws7k71uGsa2pc2rEBKCHTJY6mzvTs",
|
||||
"logo": "QmSNo8VrwLg1NRWAMhm2qc3zWFF7HK8563sAeU4Hu6DQ8u"
|
||||
"vid": "QmTuyvkgZ7H4kxLN5ws7k71uGsa2pc2rEBKCHTJY6mzvTs"
|
||||
},
|
||||
"7EC7652FCFCE8DDC6E801839291F0E28EF1D5AE7": {
|
||||
"logo": "QmPZaGnVHshwHropCW95HtNTUQSDUbfK9vFa2oQjVqcVkZ",
|
||||
"name": "Doom II",
|
||||
"vid": "Qmamv4bz1HVQWHRKbrbEWPUuAtUEhEVXJxqDYz2ui4gCaD",
|
||||
"logo": "QmPZaGnVHshwHropCW95HtNTUQSDUbfK9vFa2oQjVqcVkZ"
|
||||
},
|
||||
"9B07B02AB3C275A6A7570C3F73CC20D63A0E3833": {
|
||||
"name": "The Ultimate Doom",
|
||||
"logo": "QmNyBkiShukMmib9qndrZbovqX9ycdQE3UzZSLHSFtXdU7"
|
||||
"vid": "Qmamv4bz1HVQWHRKbrbEWPUuAtUEhEVXJxqDYz2ui4gCaD"
|
||||
},
|
||||
"90361E2A538D2388506657252AE41ACEEB1BA360": {
|
||||
"name": "Doom The Plutonia Experiment",
|
||||
"logo": "QmQEN7NGWPw4YbvS49RoQBgKNbRT9V3EynijAFcxAiJKCE"
|
||||
"logo": "QmQEN7NGWPw4YbvS49RoQBgKNbRT9V3EynijAFcxAiJKCE",
|
||||
"name": "Doom The Plutonia Experiment"
|
||||
},
|
||||
"9B07B02AB3C275A6A7570C3F73CC20D63A0E3833": {
|
||||
"logo": "QmNyBkiShukMmib9qndrZbovqX9ycdQE3UzZSLHSFtXdU7",
|
||||
"name": "The Ultimate Doom"
|
||||
},
|
||||
"9FBC66AEDEF7FE3BAE0986CDB9323D2B8DB4C9D3": {
|
||||
"name": "Doom TNT: Evilution",
|
||||
"logo": "Qma4PUdncsFtsWnFbeS1qVSyRpqsLD2J7JUaz37da2DtjQ"
|
||||
"logo": "Qma4PUdncsFtsWnFbeS1qVSyRpqsLD2J7JUaz37da2DtjQ",
|
||||
"name": "Doom TNT: Evilution"
|
||||
},
|
||||
"C6612AC5A8AC2E2A1D707F9B2869AF820EFB7C50": {
|
||||
"name": "Doom (Demo)",
|
||||
"ref": "2C8212631B37F21AD06D18B5638C733A75E179FF"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12881
metadata/gb.json
12881
metadata/gb.json
File diff suppressed because it is too large
Load Diff
25051
metadata/gba.json
25051
metadata/gba.json
File diff suppressed because it is too large
Load Diff
11471
metadata/gbc.json
11471
metadata/gbc.json
File diff suppressed because it is too large
Load Diff
@ -1,415 +1,435 @@
|
||||
{
|
||||
"F8991B0C385F4E5002FA2A7E2F5E61E8C5213356": {
|
||||
"name": "[BIOS] Atari Jaguar (World)"
|
||||
},
|
||||
"73883E7A6E9B132452436F7AB1AEAEB0776428E5": {
|
||||
"name": "[BIOS] Atari Jaguar CD (World)"
|
||||
},
|
||||
"D61B7B5912118F114EF00CF44966A5EF62E455A5": {
|
||||
"name": "[BIOS] Atari Jaguar Developer CD (World)"
|
||||
},
|
||||
"8377FF6C6527721E2E7F8458314D782925CE4590": {
|
||||
"name": "[BIOS] Atari Jaguar Stubulator '93 (World)"
|
||||
},
|
||||
"4F87982B6E16072E58A5653A3F763C8B4B3BFFCC": {
|
||||
"name": "[BIOS] Atari Jaguar Stubulator '94 (World)"
|
||||
},
|
||||
"533C497761CC802624708108BC9EF6E7C4F40296": {
|
||||
"name": "Aircars (USA)"
|
||||
},
|
||||
"4E55A5991725A4B557F4DAF75A0FA38EC667217A": {
|
||||
"name": "Aircars (USA) (Beta) (1994-11-14)"
|
||||
},
|
||||
"FD8C89250EBC1E403838B2E589D1F69E3FE2FE02": {
|
||||
"name": "Alien vs Predator (World)",
|
||||
"vid": "QmPF7SYwe3KYS9DxYHoNsVTnhwwFe9sdHkvnLdG7BS3Ff3",
|
||||
"logo": "QmcevUQ5YkCiWcNov4grof4VwqeqR8P2DH5kr7AG6E2kPM"
|
||||
},
|
||||
"80ED1EE5B8ACADDFB64A5C0BBDA2BFC14BDB5DA5": {
|
||||
"name": "Alien vs Predator (World) (Beta) (1993-08-18)"
|
||||
},
|
||||
"261667E4AAB6239ECF3A416F5C2443BF092735CC": {
|
||||
"name": "Alien vs Predator (World) (Beta) (1993-10-13)"
|
||||
},
|
||||
"1189AA22A427D81EA0BD68BF58258C8B1089614B": {
|
||||
"name": "Alien vs Predator (World) (Beta) (1994-04-08)"
|
||||
},
|
||||
"542063A5B27659781A85821F61E85CD89EA59AB3": {
|
||||
"name": "Arena Football '95 (World)"
|
||||
},
|
||||
"9C96E6C2195CE56AA930E677A041B35BD9B8BC7E": {
|
||||
"name": "Atari Karts (World)",
|
||||
"vid": "QmYYH6BQAhCkGpidXcKgqf1xnxLzynkuvTBBdBdG4K5tzo",
|
||||
"logo": "QmP4Lo8YAX9ZJ6rUJdGCoKfLByc4RKfqQt8Xd4C89J5wjy"
|
||||
},
|
||||
"1BDAAA42B38F4C93A4ABA34930E3CA406AC976C9": {
|
||||
"name": "Atari Karts (World) (Beta)"
|
||||
},
|
||||
"01DC37E7A9D03246A93BCB8C6A3116CB6391ED31": {
|
||||
"logo": "QmfKeLsqS1SBMQFwr4dRzZLuhQdskMwEFDr9jDaCMbo4wN",
|
||||
"name": "Attack of the Mutant Penguins (World)",
|
||||
"vid": "QmUZbbybLfEngb3KCkBMJ76XK7b6Wt97zo18XV9sXEWZSk",
|
||||
"logo": "QmfKeLsqS1SBMQFwr4dRzZLuhQdskMwEFDr9jDaCMbo4wN"
|
||||
},
|
||||
"18535F5985C840AEFCE0FA639131CB83A8A647F9": {
|
||||
"name": "Barkley Shut Up and Jam (World)"
|
||||
},
|
||||
"988897FD517EBD86742971205F3B29F9D70B5686": {
|
||||
"name": "Barkley Shut Up and Jam (World) (Beta)"
|
||||
},
|
||||
"8F4C964D2411A52C64EEC1D1A3061F77117BB7A6": {
|
||||
"name": "Battle Sphere (World)"
|
||||
},
|
||||
"49716A6A3A4C6ABEEA36040F26C4E8CF2A545DF7": {
|
||||
"name": "Battle Sphere Gold (World)"
|
||||
},
|
||||
"FE4C35C24BE8DF92F867B4FB42EB75A3FCC875E8": {
|
||||
"name": "Breakout 2000 (USA, Europe)"
|
||||
},
|
||||
"C8F29D04DACCE1A83A1C93AB351737795D0F55DB": {
|
||||
"name": "Breakout 2000 (USA, Europe) (Beta)"
|
||||
},
|
||||
"7F21C6DE18C46629C0D54E68143D619093B4B8D6": {
|
||||
"name": "Brett Hull Hockey (World)"
|
||||
},
|
||||
"CDFEFDF28DD9127F7A9425088A1EE3EA88DFB618": {
|
||||
"name": "Brutal Sports Football (World)",
|
||||
"vid": "QmPbw6drqyPkDuiKHThjNFH7SmPxsSDS7ago76MQwpprCX",
|
||||
"logo": "QmaqeNB3qRTvDxozJ8p7rJZKzX57TpnDjVS8JBcEkmPBs9"
|
||||
},
|
||||
"752D6E1ED185F9DB49230F42319A573A44E32397": {
|
||||
"name": "Bubsy in Fractured Furry Tales (World)",
|
||||
"vid": "Qme3hphX7hkwVJWpYwcFnxU6u2Y8J7TB9JFpKcAsMuxDQh",
|
||||
"logo": "QmcUtsD4orEMZjoPbjk3h2YTS3Ug55pF4RhvGFCbExUBcV"
|
||||
},
|
||||
"0C195676BE099F4594CE1D5A38303F6CCE899DC5": {
|
||||
"name": "Cannon Fodder (World)",
|
||||
"vid": "Qmb8XZcpytbb8jDyW617RgCtRfBUKzxx971Fac4AUh2UUa",
|
||||
"logo": "QmRFsbJZ7WKewZsSVWy5qJA6SxJbnSQDFDsRnpdXseL44K"
|
||||
},
|
||||
"4D0CC8D248A8BD0FC4B8169C5D5772E59C073C59": {
|
||||
"name": "Checkered Flag (World)",
|
||||
"vid": "Qma4iVerm6svScmwC83dpydmAYLQR1NUHTuNkns1JxEJRe",
|
||||
"logo": "QmYKfShHBmErdpVQVdWaF1bwSb6xwexpjrPrKKjRextc9t"
|
||||
},
|
||||
"BF00EC1FCA2A97BEAF28D52140DF2006B1120C67": {
|
||||
"name": "Checkered Flag (World) (Beta) (CES 1993)"
|
||||
},
|
||||
"6588F7D677463D0EF10CF905D44A2581FD9DD055": {
|
||||
"name": "Club Drive (World)"
|
||||
},
|
||||
"93A29D894F5472EB755D13E721414A6BD9686C02": {
|
||||
"name": "Club Drive (World) (Beta)"
|
||||
},
|
||||
"E00AB55F555FD1FE63B4FCE66A9B8FFE3485963E": {
|
||||
"name": "Cybermorph (World) (Rev A)",
|
||||
"vid": "QmdrHLeu5mMh6pGU1aodsTVt96UYZCwPN6H2Zi1mpZU2PG",
|
||||
"logo": "QmPAtutb1eQvfsnHRLRxk6Na5DUVNGfksxfCqsqi7CT6pE"
|
||||
},
|
||||
"C9AA59769DF207D85D9212D619C266C793C9063B": {
|
||||
"name": "Cybermorph (World) (Rev B)"
|
||||
},
|
||||
"589EFBC772360FA51EDDC096A162C8D9FE7838D3": {
|
||||
"name": "Cybermorph (World) (Beta) (1993-08-23)"
|
||||
},
|
||||
"13A4084BD5BE1CCA21D83D4C26B5B8E5CF1A3E30": {
|
||||
"name": "Defender 2000 (World)",
|
||||
"vid": "QmRw7miWyxxP1jJ3UdnXNhaattqf8j72nu5wiqmmPkdQZF",
|
||||
"logo": "QmVCsuGdk9mepCZiZ9iD7owQ4SKYSuQSomP7WMXvub5rfn"
|
||||
},
|
||||
"AE497F4839BDE6C12C87C651A80633A23D6D0713": {
|
||||
"name": "Defender 2000 (World) (Beta)"
|
||||
},
|
||||
"20DDF412E42A50D526CBBB6411BF0EC4516DB283": {
|
||||
"name": "Doom (World)",
|
||||
"vid": "QmSZgM6UhZ399co6dhiB3aGmvtq1iVr5N1SpeC792NAhUk",
|
||||
"logo": "QmZTx6wneGVU4zJTghJF7VARH89djP1RNdBUaSV8ykpMwc"
|
||||
},
|
||||
"D66B4E5F9AC27D53F0D781C741E420698630B9FC": {
|
||||
"name": "Double Dragon V - The Shadow Falls (World)",
|
||||
"vid": "QmRR3VtTRZu5DTAqFoms9VGq57x1NLsSKxnvrgWvqJiW4c",
|
||||
"logo": "QmXuVrogv2aTxNg6DFFLgei1cuCKyEH7uWdSebHFwCFF4P"
|
||||
},
|
||||
"DA7F5DAC4317AB0258F54DA436ABB8D89E530F27": {
|
||||
"name": "Dragon - The Bruce Lee Story (World)",
|
||||
"vid": "QmWPaYLEJ4X7obTauVgYqhA3dKjnGNEMYEK6TA8zRB3wM7",
|
||||
"logo": "QmWkpKyEEyizMT6vMyaUQcMpAWdW8x5ErjXqM8H84CkXZw"
|
||||
},
|
||||
"298148131AF2A86BA5D5430B1B8982710CE68874": {
|
||||
"name": "Dragon - The Bruce Lee Story (World) (Beta) (1994-09-26)"
|
||||
},
|
||||
"8F556976F42C393858127D99112264E6A5A9AE19": {
|
||||
"name": "Dragon - The Bruce Lee Story (World) (Beta) (1994-10-06)"
|
||||
},
|
||||
"179B511464ABCDA5A5DE9F971BD5CB5775D8C313": {
|
||||
"name": "Evolution - Dino Dudes (World)",
|
||||
"vid": "QmfGyzVC5mrtMxEv78czZmhzmphoEGKRrEMM7nPDp6FXbP",
|
||||
"logo": "QmVZaMvFkoR1LxXRogBRMsMaG5pe71JJVzusfSkerk8aPB"
|
||||
},
|
||||
"5BE3DA69A64B779D064D139968FADBE1C475C715": {
|
||||
"name": "Fever Pitch Soccer (World) (En,Fr,De,Es,It)",
|
||||
"vid": "QmZRt4vXXPvkKiLihkBuHgCNfnso14YPHbqhD8WsaXzw9f",
|
||||
"logo": "QmdZbMCrPHFmjWxsdwAi9VTroT7ADXG7ZmVpF97yWjjuWq"
|
||||
},
|
||||
"9F5B8844D58BB1C174DA4824EABBFD8616BA28D2": {
|
||||
"name": "Fight for Life (USA, Europe)",
|
||||
"vid": "QmehEeSFr4pVNWtSjqg6SWJNwe1eo3nJNtoQGW4pspgZ68",
|
||||
"logo": "QmTGRDYyEoyCXgZrKXbC9F1K42JujeWeYhzux3qbW7vZxh"
|
||||
},
|
||||
"37FE344383D7991D19BF2C862D29A20EAF1DA8EF": {
|
||||
"name": "Fight for Life (USA, Europe) (Beta) (1995-03-22)"
|
||||
},
|
||||
"C92F1FF8EC83EBBE0F77F5A665169408DF53B528": {
|
||||
"name": "Fight for Life (USA, Europe) (Beta) (1995-09-07)"
|
||||
},
|
||||
"824613451BE0B99020FC02E5DBA15C45E76FC8C2": {
|
||||
"name": "Flashback - The Quest for Identity (World) (En,Fr)",
|
||||
"vid": "QmSUe7urtRuLe4KkZpqXoVPsFHYsU7FiYLrwmwsfXD4QnU",
|
||||
"logo": "QmTGeRg29B8rinMjgnRXE352VnZn2qe1kaRGdwkyLbSPug"
|
||||
},
|
||||
"886CBBAA434D6E1E7FC4401BB6D176C2788C4D19": {
|
||||
"name": "Flip Out! (World)",
|
||||
"vid": "QmZDAGfvPwuWWzcn2PtWJZgrPNbnccfHj6yg8WSWvmJPyX",
|
||||
"logo": "QmW8jpj4BKCKmEd2FjGEV14rRd4mMWcmALzW1ULdaF7k3J"
|
||||
},
|
||||
"5B2295907F6FDADF5F44B1E392DCD6F5471FD095": {
|
||||
"name": "Frog Feast (World)"
|
||||
},
|
||||
"E99FF0ADD256BD45394C9ED8728E38B458CFE5E6": {
|
||||
"name": "High Voltage C Maze (USA) (Demo)"
|
||||
},
|
||||
"94E615E7A57F7C08645322870121BB32310298F5": {
|
||||
"name": "Hover Strike (World)"
|
||||
},
|
||||
"037396347D65D1A8B56E7DB361D737AB2070118F": {
|
||||
"name": "Hyper Force (World)"
|
||||
},
|
||||
"3DB485AAA28A817F468739A85E3F25741C3855AD": {
|
||||
"name": "I-War (USA, Europe)",
|
||||
"vid": "QmXXvJayEvptD1RNg5RBwYYdSv2hfV4RGRnbqVbHu5twiB",
|
||||
"logo": "QmW7ooQuquXj9SmMRnm9jzanXbyFoiXKwkczFMr38nhUet"
|
||||
},
|
||||
"F0914BE41331E3045BBC04225044DC3C73FD9DE1": {
|
||||
"name": "International Sensible Soccer (World)",
|
||||
"vid": "QmRCxSGrsU4owDTtGhg278xZhiCScgHPQFgwk9SzTrFdvs",
|
||||
"logo": "QmQ7UpHJPARawoiRfrVnheX2m3tLvFTctpFwz94gory8CT"
|
||||
},
|
||||
"8BDEFC60DD1734315E1C145E29ABF41800F908CB": {
|
||||
"name": "Iron Soldier (World)"
|
||||
},
|
||||
"47B9580EB9EEA121CF080CDBDC4AFE7FB7A19AB5": {
|
||||
"name": "Iron Soldier 2 (USA, Europe)"
|
||||
},
|
||||
"271E9411EB9E6C74240D254BDD7BAB9A039BC6D9": {
|
||||
"name": "Jack Nicklaus Cyber Golf (USA) (Proto) (1994-07-04)"
|
||||
},
|
||||
"89306ADA943C87DA740E6166F1F53837566513E2": {
|
||||
"name": "Jack Nicklaus Cyber Golf (USA) (Demo) (1995-01-13)"
|
||||
},
|
||||
"CECEC725301D7540F9392618F4FC948DEF88D398": {
|
||||
"name": "Jack Nicklaus Cyber Golf (USA) (Demo) (1995-01-16)"
|
||||
},
|
||||
"2327112B9C6F9B0E49C55DEF0ADB8119EE39CE29": {
|
||||
"name": "Kasumi Ninja (World)",
|
||||
"vid": "QmV3CbiaPDF11B52Sx6DzV4d1ZQueBztnpSDfqRWn51tHz",
|
||||
"logo": "QmaCEAzbaV8gTJtEhwc7ewwQegMZbGjHAngC6jQfS6UF9T"
|
||||
},
|
||||
"3650143BD057DD39B96E0AD0207778353F17ABE7": {
|
||||
"name": "Kasumi Ninja (World) (Demo)"
|
||||
},
|
||||
"3EA9053A63A3B0165CCEC91E85F0E699A998DC0A": {
|
||||
"name": "Missile Command 3D (World)",
|
||||
"vid": "QmZGwx2ZMDBK2XsPyrvGAujU34Fx6fqWDP85SKg8yyxLxr",
|
||||
"logo": "QmcffN7HbkieG9gXHkZ72nviP425m8xMbYh2BxCwnvLAvN"
|
||||
},
|
||||
"0E95613020D1B3A0AC76D994423C6F15DDFE9703": {
|
||||
"name": "Missile Command VR (World) (Beta) (1995-07-05)"
|
||||
},
|
||||
"3D3E6C7E552CBA5F99B6EBC15B951CE4BD956766": {
|
||||
"name": "Native (USA) (Proto)"
|
||||
},
|
||||
"B9C8FF2D26FE1E91ECD6D59B9F3F76C5B5650F26": {
|
||||
"name": "NBA Jam - Tournament Edition (USA, Europe)",
|
||||
"vid": "QmcYQvFP6WC8y6nAhVVBEjGSK1uMrof5fjSFGnNNDAosto",
|
||||
"logo": "QmbFV1DVEettBHxrAs5YfdHmKXVRhKtEVnrig3afbN5Emp"
|
||||
},
|
||||
"3EC9524921D07647F527E0A82A831F794A7D6D9B": {
|
||||
"name": "Phase Zero (World)"
|
||||
},
|
||||
"4733CF2EDE2713467F2A80B052AB94EDF6DFE534": {
|
||||
"name": "Pinball Fantasies (World)",
|
||||
"vid": "QmUX4sQRATEybMWuzwogeUjUZ7vFxGnYyhncrze5MmZ9WK",
|
||||
"logo": "QmNmKQXYJMkmwnj6mmgPAM6rsmftpNeqrh4ATzggKKJnfo"
|
||||
},
|
||||
"25611B1CFF1384DB72CC5422DBE70555E5BBD32C": {
|
||||
"name": "Pitfall - The Mayan Adventure (USA, Europe)",
|
||||
"vid": "QmPniHfXG46CEwZcGVnw9X4MxXYJd8RRe9T5qLGwT2qMUK",
|
||||
"logo": "QmXFSYwvRJqkviBDVgicJu2E2rhxYmNXhDi8dRqPtoZr8a"
|
||||
},
|
||||
"032AA31CEF116BFBA8632C14BC2FCE5BC44FF137": {
|
||||
"name": "Plucky Duck in Hollywood Hijinks (USA) (Proto) (1994-10-28)"
|
||||
},
|
||||
"75EE0AB0DABA31284A1DFF27F3C247885F5A427A": {
|
||||
"name": "Power Drive Rally (World)",
|
||||
"vid": "QmRfWpZnaUhLZjmPDuMk1JKkrvJ8EM7S72vsB64hZZveED",
|
||||
"logo": "QmWmPqprXS7jj5SmZB1cpBNTVaDxqdMzpfbMj8ZPj9iQer"
|
||||
},
|
||||
"8043E3B521EA00E806D3AB79C6C548A140944DB1": {
|
||||
"name": "Protector (World)"
|
||||
},
|
||||
"DDEA86820DCAA1A67BD82CF8023F9B3C494357B0": {
|
||||
"name": "Protector - Special Edition (World)",
|
||||
"vid": "QmNeW9TBPfpVywZ7H7YSQuGgc8bqLAHy5DWwtbPPw9eY47",
|
||||
"logo": "Qmf9zXG3HqDCwME9Nwft7BNP1MBNTNBhLwCC2NWP3EkML1"
|
||||
},
|
||||
"9D2F894DA3F25944C2F33E3A109AE8D0D951D329": {
|
||||
"name": "Raiden (World)"
|
||||
},
|
||||
"50CCF7043A702531A0E9EF86D629D5EDE5255C13": {
|
||||
"name": "Raiden (World) (Rev A)"
|
||||
},
|
||||
"D22912992FD966365A1221886E0FC5303EF04A59": {
|
||||
"name": "Rayman (World)",
|
||||
"vid": "QmbzbRxhpyotMMDtuXHruJvN1sjh1JNP8dBEtejf87cdEr",
|
||||
"logo": "QmXtq7N3BL8y2BkN2juk7Rk9FqJSFkj267XxKmP4hsNMY6"
|
||||
},
|
||||
"BA34DE71A8D278BA76BAD19964D2D886DDB94A2E": {
|
||||
"name": "Rayman (World) (Beta 2)"
|
||||
},
|
||||
"DD6F15CDA5A84E0F93BB04D24F61727A8276AE58": {
|
||||
"name": "Rayman (World) (Beta 1)"
|
||||
},
|
||||
"E0DB2FDEA7F95743C1EF452B7D289F5FE8158E33": {
|
||||
"name": "Ruiner Pinball (USA, Europe)"
|
||||
},
|
||||
"34A9941459E3B66C836F25B3EF55303734B9583E": {
|
||||
"name": "Skyhammer (World)"
|
||||
},
|
||||
"FE66FFDC9323CFCFC64DA06E16F2D5A4A4E1A236": {
|
||||
"name": "Slam Racer (USA) (Proto)"
|
||||
},
|
||||
"A738A2DF47F09C3BFDEDDFC01B35F3E8B5C5D99C": {
|
||||
"name": "Soccer Kid (World) (Beta)",
|
||||
"vid": "QmVQshtfgntHzVn8ZVZdf8i5A36YYNNqUJaLJuF6KdG1GC",
|
||||
"logo": "QmYTz3N7NpzHJQYGZ1WJ4UhBe3wFcrWHL8YMwJcZBzBEZT"
|
||||
},
|
||||
"B615FE0F229EEE89ABE4A6FCE8E4CE63D2F92C6F": {
|
||||
"name": "Soccer Kid (World)"
|
||||
},
|
||||
"6B010477155F30F1ECEC7EAB2C9A7544242BBD19": {
|
||||
"name": "Space War 2000 (World)",
|
||||
"vid": "QmPJW65sMdxtqCWQUFfwHyHU9WPuA1PNcwipA3SZ7fwwVb",
|
||||
"logo": "QmNym4uVncwQDYRwoHNMv3YHA7SeeecLqbxVPc7XLrRwgQ"
|
||||
},
|
||||
"9A58BC0C8843FD00A2871C01B91FEC206408BF80": {
|
||||
"name": "Super Burnout (World)"
|
||||
},
|
||||
"EE5FEA3B99177D9E88AEEDCAD8F01173F1062B87": {
|
||||
"name": "Super Burnout (World) (Demo) (WCES 1995)"
|
||||
},
|
||||
"C71E0BC9EEED8670C6E7CCC7A0FE3B815D7BAC83": {
|
||||
"name": "Supercross 3D (USA, Europe)",
|
||||
"vid": "QmcDoGsfHELmkyfUoTmXBwLcM83eRrwwmp8mtxjwNAD4ca",
|
||||
"logo": "QmWNmY4HTPqNX4kC7yc9UahevNSptFdC7BCg1g49c855Ye"
|
||||
},
|
||||
"A226086571C857F3796FD664BBC2C8FE99C78224": {
|
||||
"name": "Syndicate (World)",
|
||||
"vid": "QmZcBy1r8Tg2KdVPUniJHyf75LXPxhjEX1NEkksZMVWNSa",
|
||||
"logo": "QmQwKmMx7Rbvz3J7w4ChsrjoBFh2vcYEx3A8m1GdcTX8Ed"
|
||||
},
|
||||
"3828D2F953224D06B5FE7AB3644D6A75FDFE79E3": {
|
||||
"name": "Tempest 2000 (World)",
|
||||
"vid": "QmXvDFkX7YmDsHvUiJupbTiXEefFzqTQecktmnzFMRjkff",
|
||||
"logo": "QmcVjqiqF82wvqVkucrthJwhT8BsUUowFwNLYMkCNmpjtK"
|
||||
},
|
||||
"9AD03858597AA1E1FFF26250996B07D1A70D1296": {
|
||||
"name": "Thea Realm Fighters (USA) (Proto 1)"
|
||||
},
|
||||
"07C76D3F4AE05B75EA7AD62147172FD146F4F876": {
|
||||
"name": "Thea Realm Fighters (USA) (Proto 2)"
|
||||
},
|
||||
"24DA4FE776AE9DAA86CF643C703EC0F399C77521": {
|
||||
"name": "Theme Park (World)",
|
||||
"vid": "QmQJG7EmAZwZWjbZ4zL9umjpTww6pR9HdRvVgfxAZwCTn2",
|
||||
"logo": "QmdaJGvXuh8dFDgpAdW2jXSnr4pj6e7uSXULthA8t6STKq"
|
||||
},
|
||||
"841CE8AE1B71955D9B8DF3B80DA11FD40308EF5F": {
|
||||
"name": "Tiny Toon Adventures (USA) (Proto)"
|
||||
},
|
||||
"4566B33799A7C65B3E9643383D0282172FF619A5": {
|
||||
"name": "Total Carnage (World)",
|
||||
"vid": "QmaDdW5niQbiwpN4T5CDGi9x6iFtWsVLokF8vCviKZgHf5",
|
||||
"logo": "QmZfJ4Z8e4CNcmLgPKQAWEqjpRt6sqkdoKRkNyDtYdXq8o"
|
||||
},
|
||||
"1EF1B86ABF725F32267BFA140DC049CE9BA92AEA": {
|
||||
"name": "Total Carnage (World) (Beta)"
|
||||
},
|
||||
"BBE7FFB97FC07CEBE53099C8ACCE01366D7F7E15": {
|
||||
"name": "Towers II - Plight of the Stargazer (USA, Europe)"
|
||||
},
|
||||
"88E86085932AF10DFB8C94032F73A5992A30B55C": {
|
||||
"name": "Trevor McFur in the Crescent Galaxy (World) (Rev A)",
|
||||
"vid": "QmaDA59oWsebKK5xJm4MMtcE26smcfjd2CbfTGesKCuP9Q",
|
||||
"logo": "QmQkNeADHymrTFwyueXEemhWeZssNK8mkrqn5KQGipogox"
|
||||
},
|
||||
"2F0E950517D26C83408E5CF210098C0897725537": {
|
||||
"name": "Trevor McFur in the Crescent Galaxy (World) (Beta)"
|
||||
},
|
||||
"489B0FE9F070AD27F63A17AA8F0D2AFAA4DABDB3": {
|
||||
"name": "Trevor McFur in the Crescent Galaxy (World)"
|
||||
},
|
||||
"4C6A6432EA0BC198F900F4EBDB8A3A7A34475D84": {
|
||||
"name": "Troy Aikman NFL Football (World)",
|
||||
"vid": "QmTyfEp2q2FLfEX6gH27aufVfydfEtPfjYm2aJsp7LbgAj",
|
||||
"logo": "QmYFg6g3WcSCA7Akoq8dMrVCR27XxUC13iEminXUFWWXJf"
|
||||
},
|
||||
"311E41EBBA1B78F514BF16789DDB7E7E8DFB47FF": {
|
||||
"name": "Ultra Vortek (World)",
|
||||
"vid": "QmVjygYsL4ZXhhUp1V7W2FRQq6kcB8Gi8XQGfLMf1x3paU",
|
||||
"logo": "QmbKxD6MNUuRDLX8Q5Syzcwdjdfot4wjADemAf7wnD6rZw"
|
||||
},
|
||||
"E3BD433BC4A573F3B05652239A6614C3878A04D1": {
|
||||
"name": "Ultra Vortex (World) (Beta) (1995-03-20)"
|
||||
"vid": "QmUZbbybLfEngb3KCkBMJ76XK7b6Wt97zo18XV9sXEWZSk"
|
||||
},
|
||||
"02253D93EEF375C8334F1F77A2D8B72FFF7A94C6": {
|
||||
"name": "Val d'Isere Skiing and Snowboarding (World)"
|
||||
},
|
||||
"032AA31CEF116BFBA8632C14BC2FCE5BC44FF137": {
|
||||
"name": "Plucky Duck in Hollywood Hijinks (USA) (Proto) (1994-10-28)"
|
||||
},
|
||||
"037396347D65D1A8B56E7DB361D737AB2070118F": {
|
||||
"name": "Hyper Force (World)"
|
||||
},
|
||||
"07C76D3F4AE05B75EA7AD62147172FD146F4F876": {
|
||||
"name": "Thea Realm Fighters (USA) (Proto 2)"
|
||||
},
|
||||
"0C195676BE099F4594CE1D5A38303F6CCE899DC5": {
|
||||
"logo": "QmRFsbJZ7WKewZsSVWy5qJA6SxJbnSQDFDsRnpdXseL44K",
|
||||
"name": "Cannon Fodder (World)",
|
||||
"vid": "Qmb8XZcpytbb8jDyW617RgCtRfBUKzxx971Fac4AUh2UUa"
|
||||
},
|
||||
"0E95613020D1B3A0AC76D994423C6F15DDFE9703": {
|
||||
"name": "Missile Command VR (World) (Beta) (1995-07-05)"
|
||||
},
|
||||
"1189AA22A427D81EA0BD68BF58258C8B1089614B": {
|
||||
"name": "Alien vs Predator (World) (Beta) (1994-04-08)",
|
||||
"ref": "FD8C89250EBC1E403838B2E589D1F69E3FE2FE02"
|
||||
},
|
||||
"13A4084BD5BE1CCA21D83D4C26B5B8E5CF1A3E30": {
|
||||
"logo": "QmVCsuGdk9mepCZiZ9iD7owQ4SKYSuQSomP7WMXvub5rfn",
|
||||
"name": "Defender 2000 (World)",
|
||||
"vid": "QmRw7miWyxxP1jJ3UdnXNhaattqf8j72nu5wiqmmPkdQZF"
|
||||
},
|
||||
"179B511464ABCDA5A5DE9F971BD5CB5775D8C313": {
|
||||
"logo": "QmVZaMvFkoR1LxXRogBRMsMaG5pe71JJVzusfSkerk8aPB",
|
||||
"name": "Evolution - Dino Dudes (World)",
|
||||
"vid": "QmfGyzVC5mrtMxEv78czZmhzmphoEGKRrEMM7nPDp6FXbP"
|
||||
},
|
||||
"18535F5985C840AEFCE0FA639131CB83A8A647F9": {
|
||||
"name": "Barkley Shut Up and Jam (World)"
|
||||
},
|
||||
"1BDAAA42B38F4C93A4ABA34930E3CA406AC976C9": {
|
||||
"name": "Atari Karts (World) (Beta)",
|
||||
"ref": "9C96E6C2195CE56AA930E677A041B35BD9B8BC7E"
|
||||
},
|
||||
"1EF1B86ABF725F32267BFA140DC049CE9BA92AEA": {
|
||||
"name": "Total Carnage (World) (Beta)",
|
||||
"ref": "4566B33799A7C65B3E9643383D0282172FF619A5"
|
||||
},
|
||||
"20DDF412E42A50D526CBBB6411BF0EC4516DB283": {
|
||||
"logo": "QmZTx6wneGVU4zJTghJF7VARH89djP1RNdBUaSV8ykpMwc",
|
||||
"name": "Doom (World)",
|
||||
"vid": "QmSZgM6UhZ399co6dhiB3aGmvtq1iVr5N1SpeC792NAhUk"
|
||||
},
|
||||
"2327112B9C6F9B0E49C55DEF0ADB8119EE39CE29": {
|
||||
"logo": "QmaCEAzbaV8gTJtEhwc7ewwQegMZbGjHAngC6jQfS6UF9T",
|
||||
"name": "Kasumi Ninja (World)",
|
||||
"vid": "QmV3CbiaPDF11B52Sx6DzV4d1ZQueBztnpSDfqRWn51tHz"
|
||||
},
|
||||
"24DA4FE776AE9DAA86CF643C703EC0F399C77521": {
|
||||
"logo": "QmdaJGvXuh8dFDgpAdW2jXSnr4pj6e7uSXULthA8t6STKq",
|
||||
"name": "Theme Park (World)",
|
||||
"vid": "QmQJG7EmAZwZWjbZ4zL9umjpTww6pR9HdRvVgfxAZwCTn2"
|
||||
},
|
||||
"25611B1CFF1384DB72CC5422DBE70555E5BBD32C": {
|
||||
"logo": "QmXFSYwvRJqkviBDVgicJu2E2rhxYmNXhDi8dRqPtoZr8a",
|
||||
"name": "Pitfall - The Mayan Adventure (USA, Europe)",
|
||||
"vid": "QmPniHfXG46CEwZcGVnw9X4MxXYJd8RRe9T5qLGwT2qMUK"
|
||||
},
|
||||
"261667E4AAB6239ECF3A416F5C2443BF092735CC": {
|
||||
"name": "Alien vs Predator (World) (Beta) (1993-10-13)",
|
||||
"ref": "FD8C89250EBC1E403838B2E589D1F69E3FE2FE02"
|
||||
},
|
||||
"271E9411EB9E6C74240D254BDD7BAB9A039BC6D9": {
|
||||
"name": "Jack Nicklaus Cyber Golf (USA) (Proto) (1994-07-04)"
|
||||
},
|
||||
"298148131AF2A86BA5D5430B1B8982710CE68874": {
|
||||
"name": "Dragon - The Bruce Lee Story (World) (Beta) (1994-09-26)",
|
||||
"ref": "DA7F5DAC4317AB0258F54DA436ABB8D89E530F27"
|
||||
},
|
||||
"2AB78090E8AFD985CA4FE54DA8DAEF7E125550FA": {
|
||||
"name": "Virtual VCS (USA) (Proto)"
|
||||
},
|
||||
"2F0E950517D26C83408E5CF210098C0897725537": {
|
||||
"name": "Trevor McFur in the Crescent Galaxy (World) (Beta)",
|
||||
"ref": "88E86085932AF10DFB8C94032F73A5992A30B55C"
|
||||
},
|
||||
"311E41EBBA1B78F514BF16789DDB7E7E8DFB47FF": {
|
||||
"logo": "QmbKxD6MNUuRDLX8Q5Syzcwdjdfot4wjADemAf7wnD6rZw",
|
||||
"name": "Ultra Vortek (World)",
|
||||
"vid": "QmVjygYsL4ZXhhUp1V7W2FRQq6kcB8Gi8XQGfLMf1x3paU"
|
||||
},
|
||||
"32FF3811F8F6DF8A2BB56165C62227A3A480E55C": {
|
||||
"logo": "QmSRcjgwAWLouMTYq6GFQQSghH28yg8fGQHwYjbj7nA2w7",
|
||||
"name": "Zool 2 (World)",
|
||||
"vid": "Qmerp8YE5HYS4MyP8jnjY2YMNJbChZr2jGbuttMsXz16F4"
|
||||
},
|
||||
"34A9941459E3B66C836F25B3EF55303734B9583E": {
|
||||
"name": "Skyhammer (World)"
|
||||
},
|
||||
"3650143BD057DD39B96E0AD0207778353F17ABE7": {
|
||||
"name": "Kasumi Ninja (World) (Demo)",
|
||||
"ref": "2327112B9C6F9B0E49C55DEF0ADB8119EE39CE29"
|
||||
},
|
||||
"37FE344383D7991D19BF2C862D29A20EAF1DA8EF": {
|
||||
"name": "Fight for Life (USA, Europe) (Beta) (1995-03-22)",
|
||||
"ref": "9F5B8844D58BB1C174DA4824EABBFD8616BA28D2"
|
||||
},
|
||||
"3828D2F953224D06B5FE7AB3644D6A75FDFE79E3": {
|
||||
"logo": "QmcVjqiqF82wvqVkucrthJwhT8BsUUowFwNLYMkCNmpjtK",
|
||||
"name": "Tempest 2000 (World)",
|
||||
"vid": "QmXvDFkX7YmDsHvUiJupbTiXEefFzqTQecktmnzFMRjkff"
|
||||
},
|
||||
"3D3E6C7E552CBA5F99B6EBC15B951CE4BD956766": {
|
||||
"name": "Native (USA) (Proto)"
|
||||
},
|
||||
"3DB485AAA28A817F468739A85E3F25741C3855AD": {
|
||||
"logo": "QmW7ooQuquXj9SmMRnm9jzanXbyFoiXKwkczFMr38nhUet",
|
||||
"name": "I-War (USA, Europe)",
|
||||
"vid": "QmXXvJayEvptD1RNg5RBwYYdSv2hfV4RGRnbqVbHu5twiB"
|
||||
},
|
||||
"3EA9053A63A3B0165CCEC91E85F0E699A998DC0A": {
|
||||
"logo": "QmcffN7HbkieG9gXHkZ72nviP425m8xMbYh2BxCwnvLAvN",
|
||||
"name": "Missile Command 3D (World)",
|
||||
"vid": "QmZGwx2ZMDBK2XsPyrvGAujU34Fx6fqWDP85SKg8yyxLxr"
|
||||
},
|
||||
"3EC9524921D07647F527E0A82A831F794A7D6D9B": {
|
||||
"name": "Phase Zero (World)"
|
||||
},
|
||||
"4566B33799A7C65B3E9643383D0282172FF619A5": {
|
||||
"logo": "QmZfJ4Z8e4CNcmLgPKQAWEqjpRt6sqkdoKRkNyDtYdXq8o",
|
||||
"name": "Total Carnage (World)",
|
||||
"vid": "QmaDdW5niQbiwpN4T5CDGi9x6iFtWsVLokF8vCviKZgHf5"
|
||||
},
|
||||
"4733CF2EDE2713467F2A80B052AB94EDF6DFE534": {
|
||||
"logo": "QmNmKQXYJMkmwnj6mmgPAM6rsmftpNeqrh4ATzggKKJnfo",
|
||||
"name": "Pinball Fantasies (World)",
|
||||
"vid": "QmUX4sQRATEybMWuzwogeUjUZ7vFxGnYyhncrze5MmZ9WK"
|
||||
},
|
||||
"47B9580EB9EEA121CF080CDBDC4AFE7FB7A19AB5": {
|
||||
"name": "Iron Soldier 2 (USA, Europe)"
|
||||
},
|
||||
"489B0FE9F070AD27F63A17AA8F0D2AFAA4DABDB3": {
|
||||
"name": "Trevor McFur in the Crescent Galaxy (World)",
|
||||
"ref": "88E86085932AF10DFB8C94032F73A5992A30B55C"
|
||||
},
|
||||
"49716A6A3A4C6ABEEA36040F26C4E8CF2A545DF7": {
|
||||
"name": "Battle Sphere Gold (World)"
|
||||
},
|
||||
"4C6A6432EA0BC198F900F4EBDB8A3A7A34475D84": {
|
||||
"logo": "QmYFg6g3WcSCA7Akoq8dMrVCR27XxUC13iEminXUFWWXJf",
|
||||
"name": "Troy Aikman NFL Football (World)",
|
||||
"vid": "QmTyfEp2q2FLfEX6gH27aufVfydfEtPfjYm2aJsp7LbgAj"
|
||||
},
|
||||
"4D0CC8D248A8BD0FC4B8169C5D5772E59C073C59": {
|
||||
"logo": "QmYKfShHBmErdpVQVdWaF1bwSb6xwexpjrPrKKjRextc9t",
|
||||
"name": "Checkered Flag (World)",
|
||||
"vid": "Qma4iVerm6svScmwC83dpydmAYLQR1NUHTuNkns1JxEJRe"
|
||||
},
|
||||
"4D16596AC2D991435599E1FE03292ADFB25C346D": {
|
||||
"name": "White Men Can't Jump (USA, Europe)"
|
||||
},
|
||||
"EE553176F0A32683B517B84B12C6FAE13C15C3D0": {
|
||||
"name": "Wolfenstein 3D (World)"
|
||||
"4E55A5991725A4B557F4DAF75A0FA38EC667217A": {
|
||||
"name": "Aircars (USA) (Beta) (1994-11-14)"
|
||||
},
|
||||
"641B07F93E8DF03AB8B9BF1E8DC56E2889F247F8": {
|
||||
"name": "Worms (USA, Europe)",
|
||||
"vid": "QmcahTbiuzvNm1ZXSoVKiULmxpGGjCAN3ZQpcxgHZFQ6RB",
|
||||
"logo": "QmNXexYonZXnNi4kKfDezNkX1Dg8tuU3iN7518sFtupyre"
|
||||
"4F87982B6E16072E58A5653A3F763C8B4B3BFFCC": {
|
||||
"name": "[BIOS] Atari Jaguar Stubulator '94 (World)"
|
||||
},
|
||||
"B68DE2CD49CB70E92F2415837EDFA7AB46770A78": {
|
||||
"name": "Zero 5 (USA, Europe)",
|
||||
"vid": "QmQjnfGuuznkcmCTP54nKsEMfvPVDKiBvYvWE4KwauxdAF",
|
||||
"logo": "QmawCN7c7wHCrM4e6XHixQgDxLyQUXbfXWZk8Wc3Zakbeg"
|
||||
},
|
||||
"FB03CDB0B9C8171831C0E385C27BEC2D71C39400": {
|
||||
"name": "Zero 5 (USA, Europe) (Beta)"
|
||||
},
|
||||
"32FF3811F8F6DF8A2BB56165C62227A3A480E55C": {
|
||||
"name": "Zool 2 (World)",
|
||||
"vid": "Qmerp8YE5HYS4MyP8jnjY2YMNJbChZr2jGbuttMsXz16F4",
|
||||
"logo": "QmSRcjgwAWLouMTYq6GFQQSghH28yg8fGQHwYjbj7nA2w7"
|
||||
"50CCF7043A702531A0E9EF86D629D5EDE5255C13": {
|
||||
"name": "Raiden (World) (Rev A)"
|
||||
},
|
||||
"52EC786F7536AB043DC24725AF94068CAD2B0EC3": {
|
||||
"name": "Zoop (USA, Europe)"
|
||||
},
|
||||
"7E4356FECBEADD741C2BF9868A37B2956BD27D5E": {
|
||||
"name": "Zzyorxx II (USA) (Demo)"
|
||||
"533C497761CC802624708108BC9EF6E7C4F40296": {
|
||||
"name": "Aircars (USA)"
|
||||
},
|
||||
"542063A5B27659781A85821F61E85CD89EA59AB3": {
|
||||
"name": "Arena Football '95 (World)"
|
||||
},
|
||||
"589EFBC772360FA51EDDC096A162C8D9FE7838D3": {
|
||||
"name": "Cybermorph (World) (Beta) (1993-08-23)",
|
||||
"ref": "E00AB55F555FD1FE63B4FCE66A9B8FFE3485963E"
|
||||
},
|
||||
"5B2295907F6FDADF5F44B1E392DCD6F5471FD095": {
|
||||
"name": "Frog Feast (World)"
|
||||
},
|
||||
"5BE3DA69A64B779D064D139968FADBE1C475C715": {
|
||||
"logo": "QmdZbMCrPHFmjWxsdwAi9VTroT7ADXG7ZmVpF97yWjjuWq",
|
||||
"name": "Fever Pitch Soccer (World) (En,Fr,De,Es,It)",
|
||||
"vid": "QmZRt4vXXPvkKiLihkBuHgCNfnso14YPHbqhD8WsaXzw9f"
|
||||
},
|
||||
"61D4DB841C090AE3104EB20B6087160FF80E7A12": {
|
||||
"ref": "886CBBAA434D6E1E7FC4401BB6D176C2788C4D19"
|
||||
},
|
||||
"641B07F93E8DF03AB8B9BF1E8DC56E2889F247F8": {
|
||||
"logo": "QmNXexYonZXnNi4kKfDezNkX1Dg8tuU3iN7518sFtupyre",
|
||||
"name": "Worms (USA, Europe)",
|
||||
"vid": "QmcahTbiuzvNm1ZXSoVKiULmxpGGjCAN3ZQpcxgHZFQ6RB"
|
||||
},
|
||||
"6588F7D677463D0EF10CF905D44A2581FD9DD055": {
|
||||
"name": "Club Drive (World)"
|
||||
},
|
||||
"6B010477155F30F1ECEC7EAB2C9A7544242BBD19": {
|
||||
"logo": "QmNym4uVncwQDYRwoHNMv3YHA7SeeecLqbxVPc7XLrRwgQ",
|
||||
"name": "Space War 2000 (World)",
|
||||
"vid": "QmPJW65sMdxtqCWQUFfwHyHU9WPuA1PNcwipA3SZ7fwwVb"
|
||||
},
|
||||
"73883E7A6E9B132452436F7AB1AEAEB0776428E5": {
|
||||
"name": "[BIOS] Atari Jaguar CD (World)"
|
||||
},
|
||||
"752D6E1ED185F9DB49230F42319A573A44E32397": {
|
||||
"logo": "QmcUtsD4orEMZjoPbjk3h2YTS3Ug55pF4RhvGFCbExUBcV",
|
||||
"name": "Bubsy in Fractured Furry Tales (World)",
|
||||
"vid": "Qme3hphX7hkwVJWpYwcFnxU6u2Y8J7TB9JFpKcAsMuxDQh"
|
||||
},
|
||||
"75EE0AB0DABA31284A1DFF27F3C247885F5A427A": {
|
||||
"logo": "QmWmPqprXS7jj5SmZB1cpBNTVaDxqdMzpfbMj8ZPj9iQer",
|
||||
"name": "Power Drive Rally (World)",
|
||||
"vid": "QmRfWpZnaUhLZjmPDuMk1JKkrvJ8EM7S72vsB64hZZveED"
|
||||
},
|
||||
"7E4356FECBEADD741C2BF9868A37B2956BD27D5E": {
|
||||
"name": "Zzyorxx II (USA) (Demo)"
|
||||
},
|
||||
"7F21C6DE18C46629C0D54E68143D619093B4B8D6": {
|
||||
"name": "Brett Hull Hockey (World)"
|
||||
},
|
||||
"8043E3B521EA00E806D3AB79C6C548A140944DB1": {
|
||||
"name": "Protector (World)"
|
||||
},
|
||||
"80ED1EE5B8ACADDFB64A5C0BBDA2BFC14BDB5DA5": {
|
||||
"name": "Alien vs Predator (World) (Beta) (1993-08-18)",
|
||||
"ref": "FD8C89250EBC1E403838B2E589D1F69E3FE2FE02"
|
||||
},
|
||||
"824613451BE0B99020FC02E5DBA15C45E76FC8C2": {
|
||||
"logo": "QmTGeRg29B8rinMjgnRXE352VnZn2qe1kaRGdwkyLbSPug",
|
||||
"name": "Flashback - The Quest for Identity (World) (En,Fr)",
|
||||
"vid": "QmSUe7urtRuLe4KkZpqXoVPsFHYsU7FiYLrwmwsfXD4QnU"
|
||||
},
|
||||
"8377FF6C6527721E2E7F8458314D782925CE4590": {
|
||||
"name": "[BIOS] Atari Jaguar Stubulator '93 (World)"
|
||||
},
|
||||
"841CE8AE1B71955D9B8DF3B80DA11FD40308EF5F": {
|
||||
"name": "Tiny Toon Adventures (USA) (Proto)"
|
||||
},
|
||||
"886CBBAA434D6E1E7FC4401BB6D176C2788C4D19": {
|
||||
"logo": "QmW8jpj4BKCKmEd2FjGEV14rRd4mMWcmALzW1ULdaF7k3J",
|
||||
"name": "Flip Out! (World)",
|
||||
"vid": "QmZDAGfvPwuWWzcn2PtWJZgrPNbnccfHj6yg8WSWvmJPyX"
|
||||
},
|
||||
"88E86085932AF10DFB8C94032F73A5992A30B55C": {
|
||||
"logo": "QmQkNeADHymrTFwyueXEemhWeZssNK8mkrqn5KQGipogox",
|
||||
"name": "Trevor McFur in the Crescent Galaxy (World) (Rev A)",
|
||||
"vid": "QmaDA59oWsebKK5xJm4MMtcE26smcfjd2CbfTGesKCuP9Q"
|
||||
},
|
||||
"89306ADA943C87DA740E6166F1F53837566513E2": {
|
||||
"name": "Jack Nicklaus Cyber Golf (USA) (Demo) (1995-01-13)"
|
||||
},
|
||||
"8BDEFC60DD1734315E1C145E29ABF41800F908CB": {
|
||||
"name": "Iron Soldier (World)"
|
||||
},
|
||||
"8F4C964D2411A52C64EEC1D1A3061F77117BB7A6": {
|
||||
"name": "Battle Sphere (World)"
|
||||
},
|
||||
"8F556976F42C393858127D99112264E6A5A9AE19": {
|
||||
"name": "Dragon - The Bruce Lee Story (World) (Beta) (1994-10-06)",
|
||||
"ref": "DA7F5DAC4317AB0258F54DA436ABB8D89E530F27"
|
||||
},
|
||||
"93A29D894F5472EB755D13E721414A6BD9686C02": {
|
||||
"name": "Club Drive (World) (Beta)"
|
||||
},
|
||||
"94E615E7A57F7C08645322870121BB32310298F5": {
|
||||
"name": "Hover Strike (World)"
|
||||
},
|
||||
"988897FD517EBD86742971205F3B29F9D70B5686": {
|
||||
"name": "Barkley Shut Up and Jam (World) (Beta)"
|
||||
},
|
||||
"9A58BC0C8843FD00A2871C01B91FEC206408BF80": {
|
||||
"name": "Super Burnout (World)"
|
||||
},
|
||||
"9AD03858597AA1E1FFF26250996B07D1A70D1296": {
|
||||
"name": "Thea Realm Fighters (USA) (Proto 1)"
|
||||
},
|
||||
"9C96E6C2195CE56AA930E677A041B35BD9B8BC7E": {
|
||||
"logo": "QmP4Lo8YAX9ZJ6rUJdGCoKfLByc4RKfqQt8Xd4C89J5wjy",
|
||||
"name": "Atari Karts (World)",
|
||||
"vid": "QmYYH6BQAhCkGpidXcKgqf1xnxLzynkuvTBBdBdG4K5tzo"
|
||||
},
|
||||
"9D2F894DA3F25944C2F33E3A109AE8D0D951D329": {
|
||||
"name": "Raiden (World)"
|
||||
},
|
||||
"9F5B8844D58BB1C174DA4824EABBFD8616BA28D2": {
|
||||
"logo": "QmTGRDYyEoyCXgZrKXbC9F1K42JujeWeYhzux3qbW7vZxh",
|
||||
"name": "Fight for Life (USA, Europe)",
|
||||
"vid": "QmehEeSFr4pVNWtSjqg6SWJNwe1eo3nJNtoQGW4pspgZ68"
|
||||
},
|
||||
"A226086571C857F3796FD664BBC2C8FE99C78224": {
|
||||
"logo": "QmQwKmMx7Rbvz3J7w4ChsrjoBFh2vcYEx3A8m1GdcTX8Ed",
|
||||
"name": "Syndicate (World)",
|
||||
"vid": "QmZcBy1r8Tg2KdVPUniJHyf75LXPxhjEX1NEkksZMVWNSa"
|
||||
},
|
||||
"A738A2DF47F09C3BFDEDDFC01B35F3E8B5C5D99C": {
|
||||
"logo": "QmYTz3N7NpzHJQYGZ1WJ4UhBe3wFcrWHL8YMwJcZBzBEZT",
|
||||
"name": "Soccer Kid (World) (Beta)",
|
||||
"vid": "QmVQshtfgntHzVn8ZVZdf8i5A36YYNNqUJaLJuF6KdG1GC"
|
||||
},
|
||||
"AE497F4839BDE6C12C87C651A80633A23D6D0713": {
|
||||
"name": "Defender 2000 (World) (Beta)",
|
||||
"ref": "13A4084BD5BE1CCA21D83D4C26B5B8E5CF1A3E30"
|
||||
},
|
||||
"B5DA0B1231A6E6532268C4E04F74E56F047DD5A5": {
|
||||
"ref": "6B010477155F30F1ECEC7EAB2C9A7544242BBD19"
|
||||
},
|
||||
"B615FE0F229EEE89ABE4A6FCE8E4CE63D2F92C6F": {
|
||||
"name": "Soccer Kid (World)",
|
||||
"ref": "A738A2DF47F09C3BFDEDDFC01B35F3E8B5C5D99C"
|
||||
},
|
||||
"B68DE2CD49CB70E92F2415837EDFA7AB46770A78": {
|
||||
"logo": "QmawCN7c7wHCrM4e6XHixQgDxLyQUXbfXWZk8Wc3Zakbeg",
|
||||
"name": "Zero 5 (USA, Europe)",
|
||||
"vid": "QmQjnfGuuznkcmCTP54nKsEMfvPVDKiBvYvWE4KwauxdAF"
|
||||
},
|
||||
"B9C8FF2D26FE1E91ECD6D59B9F3F76C5B5650F26": {
|
||||
"logo": "QmbFV1DVEettBHxrAs5YfdHmKXVRhKtEVnrig3afbN5Emp",
|
||||
"name": "NBA Jam - Tournament Edition (USA, Europe)",
|
||||
"vid": "QmcYQvFP6WC8y6nAhVVBEjGSK1uMrof5fjSFGnNNDAosto"
|
||||
},
|
||||
"BA34DE71A8D278BA76BAD19964D2D886DDB94A2E": {
|
||||
"name": "Rayman (World) (Beta 2)",
|
||||
"ref": "D22912992FD966365A1221886E0FC5303EF04A59"
|
||||
},
|
||||
"BBE7FFB97FC07CEBE53099C8ACCE01366D7F7E15": {
|
||||
"name": "Towers II - Plight of the Stargazer (USA, Europe)"
|
||||
},
|
||||
"BF00EC1FCA2A97BEAF28D52140DF2006B1120C67": {
|
||||
"name": "Checkered Flag (World) (Beta) (CES 1993)",
|
||||
"ref": "4D0CC8D248A8BD0FC4B8169C5D5772E59C073C59"
|
||||
},
|
||||
"C71E0BC9EEED8670C6E7CCC7A0FE3B815D7BAC83": {
|
||||
"logo": "QmWNmY4HTPqNX4kC7yc9UahevNSptFdC7BCg1g49c855Ye",
|
||||
"name": "Supercross 3D (USA, Europe)",
|
||||
"vid": "QmcDoGsfHELmkyfUoTmXBwLcM83eRrwwmp8mtxjwNAD4ca"
|
||||
},
|
||||
"C8F29D04DACCE1A83A1C93AB351737795D0F55DB": {
|
||||
"name": "Breakout 2000 (USA, Europe) (Beta)"
|
||||
},
|
||||
"C92F1FF8EC83EBBE0F77F5A665169408DF53B528": {
|
||||
"name": "Fight for Life (USA, Europe) (Beta) (1995-09-07)",
|
||||
"ref": "9F5B8844D58BB1C174DA4824EABBFD8616BA28D2"
|
||||
},
|
||||
"C9AA59769DF207D85D9212D619C266C793C9063B": {
|
||||
"name": "Cybermorph (World) (Rev B)",
|
||||
"ref": "E00AB55F555FD1FE63B4FCE66A9B8FFE3485963E"
|
||||
},
|
||||
"CDFEFDF28DD9127F7A9425088A1EE3EA88DFB618": {
|
||||
"logo": "QmaqeNB3qRTvDxozJ8p7rJZKzX57TpnDjVS8JBcEkmPBs9",
|
||||
"name": "Brutal Sports Football (World)",
|
||||
"vid": "QmPbw6drqyPkDuiKHThjNFH7SmPxsSDS7ago76MQwpprCX"
|
||||
},
|
||||
"CECEC725301D7540F9392618F4FC948DEF88D398": {
|
||||
"name": "Jack Nicklaus Cyber Golf (USA) (Demo) (1995-01-16)"
|
||||
},
|
||||
"D22912992FD966365A1221886E0FC5303EF04A59": {
|
||||
"logo": "QmXtq7N3BL8y2BkN2juk7Rk9FqJSFkj267XxKmP4hsNMY6",
|
||||
"name": "Rayman (World)",
|
||||
"vid": "QmbzbRxhpyotMMDtuXHruJvN1sjh1JNP8dBEtejf87cdEr"
|
||||
},
|
||||
"D61B7B5912118F114EF00CF44966A5EF62E455A5": {
|
||||
"name": "[BIOS] Atari Jaguar Developer CD (World)"
|
||||
},
|
||||
"D66B4E5F9AC27D53F0D781C741E420698630B9FC": {
|
||||
"logo": "QmXuVrogv2aTxNg6DFFLgei1cuCKyEH7uWdSebHFwCFF4P",
|
||||
"name": "Double Dragon V - The Shadow Falls (World)",
|
||||
"vid": "QmRR3VtTRZu5DTAqFoms9VGq57x1NLsSKxnvrgWvqJiW4c"
|
||||
},
|
||||
"DA7F5DAC4317AB0258F54DA436ABB8D89E530F27": {
|
||||
"logo": "QmWkpKyEEyizMT6vMyaUQcMpAWdW8x5ErjXqM8H84CkXZw",
|
||||
"name": "Dragon - The Bruce Lee Story (World)",
|
||||
"vid": "QmWPaYLEJ4X7obTauVgYqhA3dKjnGNEMYEK6TA8zRB3wM7"
|
||||
},
|
||||
"DD6F15CDA5A84E0F93BB04D24F61727A8276AE58": {
|
||||
"name": "Rayman (World) (Beta 1)",
|
||||
"ref": "D22912992FD966365A1221886E0FC5303EF04A59"
|
||||
},
|
||||
"DDEA86820DCAA1A67BD82CF8023F9B3C494357B0": {
|
||||
"logo": "Qmf9zXG3HqDCwME9Nwft7BNP1MBNTNBhLwCC2NWP3EkML1",
|
||||
"name": "Protector - Special Edition (World)",
|
||||
"vid": "QmNeW9TBPfpVywZ7H7YSQuGgc8bqLAHy5DWwtbPPw9eY47"
|
||||
},
|
||||
"E00AB55F555FD1FE63B4FCE66A9B8FFE3485963E": {
|
||||
"logo": "QmPAtutb1eQvfsnHRLRxk6Na5DUVNGfksxfCqsqi7CT6pE",
|
||||
"name": "Cybermorph (World) (Rev A)",
|
||||
"vid": "QmdrHLeu5mMh6pGU1aodsTVt96UYZCwPN6H2Zi1mpZU2PG"
|
||||
},
|
||||
"E0DB2FDEA7F95743C1EF452B7D289F5FE8158E33": {
|
||||
"name": "Ruiner Pinball (USA, Europe)"
|
||||
},
|
||||
"E3BD433BC4A573F3B05652239A6614C3878A04D1": {
|
||||
"name": "Ultra Vortex (World) (Beta) (1995-03-20)"
|
||||
},
|
||||
"E99FF0ADD256BD45394C9ED8728E38B458CFE5E6": {
|
||||
"name": "High Voltage C Maze (USA) (Demo)"
|
||||
},
|
||||
"EE553176F0A32683B517B84B12C6FAE13C15C3D0": {
|
||||
"name": "Wolfenstein 3D (World)"
|
||||
},
|
||||
"EE5FEA3B99177D9E88AEEDCAD8F01173F1062B87": {
|
||||
"name": "Super Burnout (World) (Demo) (WCES 1995)"
|
||||
},
|
||||
"F0914BE41331E3045BBC04225044DC3C73FD9DE1": {
|
||||
"logo": "QmQ7UpHJPARawoiRfrVnheX2m3tLvFTctpFwz94gory8CT",
|
||||
"name": "International Sensible Soccer (World)",
|
||||
"vid": "QmRCxSGrsU4owDTtGhg278xZhiCScgHPQFgwk9SzTrFdvs"
|
||||
},
|
||||
"F8991B0C385F4E5002FA2A7E2F5E61E8C5213356": {
|
||||
"name": "[BIOS] Atari Jaguar (World)"
|
||||
},
|
||||
"FB03CDB0B9C8171831C0E385C27BEC2D71C39400": {
|
||||
"name": "Zero 5 (USA, Europe) (Beta)",
|
||||
"ref": "B68DE2CD49CB70E92F2415837EDFA7AB46770A78"
|
||||
},
|
||||
"FD8C89250EBC1E403838B2E589D1F69E3FE2FE02": {
|
||||
"logo": "QmcevUQ5YkCiWcNov4grof4VwqeqR8P2DH5kr7AG6E2kPM",
|
||||
"name": "Alien vs Predator (World)",
|
||||
"vid": "QmPF7SYwe3KYS9DxYHoNsVTnhwwFe9sdHkvnLdG7BS3Ff3"
|
||||
},
|
||||
"FE4C35C24BE8DF92F867B4FB42EB75A3FCC875E8": {
|
||||
"name": "Breakout 2000 (USA, Europe)"
|
||||
},
|
||||
"FE66FFDC9323CFCFC64DA06E16F2D5A4A4E1A236": {
|
||||
"name": "Slam Racer (USA) (Proto)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1512
metadata/lynx.json
1512
metadata/lynx.json
File diff suppressed because it is too large
Load Diff
8125
metadata/msx.json
8125
metadata/msx.json
File diff suppressed because it is too large
Load Diff
8273
metadata/n64.json
8273
metadata/n64.json
File diff suppressed because it is too large
Load Diff
51945
metadata/nds.json
51945
metadata/nds.json
File diff suppressed because it is too large
Load Diff
26497
metadata/nes.json
26497
metadata/nes.json
File diff suppressed because it is too large
Load Diff
1010
metadata/ngp.json
1010
metadata/ngp.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6732
metadata/pce.json
6732
metadata/pce.json
File diff suppressed because it is too large
Load Diff
67933
metadata/psx.json
67933
metadata/psx.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4059
metadata/segaCD.json
4059
metadata/segaCD.json
File diff suppressed because it is too large
Load Diff
6085
metadata/segaGG.json
6085
metadata/segaGG.json
File diff suppressed because it is too large
Load Diff
19762
metadata/segaMD.json
19762
metadata/segaMD.json
File diff suppressed because it is too large
Load Diff
4763
metadata/segaMS.json
4763
metadata/segaMS.json
File diff suppressed because it is too large
Load Diff
1809
metadata/segaSG.json
1809
metadata/segaSG.json
File diff suppressed because it is too large
Load Diff
13836
metadata/segaSaturn.json
13836
metadata/segaSaturn.json
File diff suppressed because it is too large
Load Diff
28889
metadata/snes.json
28889
metadata/snes.json
File diff suppressed because it is too large
Load Diff
224
metadata/vb.json
224
metadata/vb.json
@ -1,143 +1,149 @@
|
||||
{
|
||||
"5177015A91442E56BD76AF39447BCA365E06C272": {
|
||||
"name": "3-D Tetris (USA)",
|
||||
"vid": "QmcsARDkyozauAacURMi33H5TXAtraTWGJzPmJv2onvGQJ",
|
||||
"logo": "QmYB88E8a8SfCrFhJ7jaEkU6arv4prof96iMbKQSUrD5Ww"
|
||||
"0E086D7EF2BD8B97315196943FD0B71DA07AA8F1": {
|
||||
"logo": "QmZPPnoAR8NcAps63MrRsxN3A9kej6ghDunoRg77fF4ZvA",
|
||||
"name": "Jack Bros. (USA)",
|
||||
"vid": "QmfYBbFiGsguYMxDoJZQGyzYCcDMhqvLynivFXj9QPgrbp"
|
||||
},
|
||||
"1196EF8B9A96A0FF2F3634F8EB1C4865C0FB6B01": {
|
||||
"name": "Bound High (Japan) (En) (Proto)"
|
||||
},
|
||||
"93A25ABF4F86E71A49C1C3A470140BB40CB693D6": {
|
||||
"name": "Galactic Pinball (Japan, USA)",
|
||||
"vid": "QmcZ6zTS3VPzSMRjTxQUkD5zeRssNosfL1BKbPvbHeRzXi",
|
||||
"logo": "QmfPLMFW7RHrNQbZKj6kME6fA3p1rQJvvPzCdMYVmkf5Bf"
|
||||
"16AD904FAAFD24642DFD056D10A768E81F0D9BFA": {
|
||||
"logo": "QmUpBKPjXguAkS69F1ZQRoX5D3kRznYoEVYF8bFqLpBA1R",
|
||||
"name": "Space Invaders - Virtual Collection (Japan)",
|
||||
"vid": "QmUe2GCnx99s9DZxaQtHmtNrzRJ7YvHGi1NuqiAtDf46cj"
|
||||
},
|
||||
"23CE3C174789CDD306497D86CB2C4E76BA8B06E5": {
|
||||
"name": "Golf (USA)",
|
||||
"vid": "QmVvzYoiK3xesgZveFqRhmQ14v11jcdAsTQj9rTf5DyW7v",
|
||||
"logo": "Qmdwyb2mL1zDo1BMnNVFTGBgsrzZZjmjWeop9pcfP88zhK"
|
||||
},
|
||||
"578355DE83822CE67522033DD7545631124A71AB": {
|
||||
"name": "Hyper Fighting (World) (Aftermarket) (Unl) (Pirate)",
|
||||
"vid": "QmfBs2QTLxhsN5LtpVxW4NUUWp9qEn5o69xXKc8Qd4cCHg",
|
||||
"logo": "QmQPUUg8cHNUmbwChaYV8ejbdoo8kW3G6smRsauUVKectJ"
|
||||
},
|
||||
"322DC4C2DF74BF19D8A30D43C489401BE5E76CA5": {
|
||||
"name": "Hyper Fighting (World) (Beta) (Aftermarket) (Unl) (Pirate)"
|
||||
},
|
||||
"37E06E53CE2810E870835C48DECDD6C72079BFDA": {
|
||||
"name": "Innsmouth no Yakata (Japan)",
|
||||
"vid": "QmS7VL589BMEasXVM3tBDwfEjqUpcATcMP4GN4RMauh5yw",
|
||||
"logo": "QmV2hr6f1Sf2ZkfmtjYXDPTRjXyQ3qa3XvmxHqmYcUnLMP"
|
||||
},
|
||||
"0E086D7EF2BD8B97315196943FD0B71DA07AA8F1": {
|
||||
"name": "Jack Bros. (USA)",
|
||||
"vid": "QmfYBbFiGsguYMxDoJZQGyzYCcDMhqvLynivFXj9QPgrbp",
|
||||
"logo": "QmZPPnoAR8NcAps63MrRsxN3A9kej6ghDunoRg77fF4ZvA"
|
||||
},
|
||||
"A973406590382EE503037271330D68D2834B79DB": {
|
||||
"name": "Jack Bros. no Meiro de Hiihoo! (Japan)"
|
||||
},
|
||||
"7556A778B60490BDB81774BCBAA7413FC84CB985": {
|
||||
"name": "Mario Clash (Japan, USA)",
|
||||
"vid": "QmXMujzzbxGLUNjVeFY9rexf8F2HuVyUR53tM3dLmHw6Up",
|
||||
"logo": "QmXiFK6k798oYjYPYpG4zZ3wc6YnhcmrM2Mw4XHNxXyyXr"
|
||||
},
|
||||
"5162F7FA9E3EAE4338C53CCBA641BA080C768754": {
|
||||
"name": "Mario's Tennis (Japan, USA)",
|
||||
"vid": "QmV46x6EkJUYw69Li1KC2yGuDWKcoPVRAvXoUZGfr58Sjq",
|
||||
"logo": "QmeeJjkBUR8vXjvxo8xwixChLpeNShxiWT9wjeaafNu54x"
|
||||
},
|
||||
"F4A4C7928F15102CF14C90C5F044E5F7CC7C32F9": {
|
||||
"name": "Nester's Funky Bowling (USA)",
|
||||
"vid": "Qmd115CebnftLvbWYyiNdBXSgj45tHPqezmqgZF4EaHhkp",
|
||||
"logo": "QmYhPDmeG8cj88j2JMyvtgRqXD6Jj5ee4g55mfRAphVeUS"
|
||||
"1A67F6BDB45DB46E22B8AADEA754A68AD379AE29": {
|
||||
"logo": "QmPoFytQpiAoJ9nW4ZrjjJMVdAcZyME7SRRVDSnK5dMkPc",
|
||||
"name": "SD Gundam - Dimension War (Japan)",
|
||||
"vid": "QmR2dDWQ3GrT842baDxNQHP6N6wmfheZGZ2fqYWHRK2yYF"
|
||||
},
|
||||
"217CA062F65D4E8B2848085918AF5AC4B33B12D4": {
|
||||
"name": "Niko-chan Battle (Japan) (Proto)"
|
||||
},
|
||||
"80216B2874CF162F301D571BB8AEBC98D80B4F3E": {
|
||||
"name": "Panic Bomber (USA)",
|
||||
"vid": "QmS46otKp9pMfQFz147Qdp7QRq9LF4k1wHKU7nFq9pKc3F",
|
||||
"logo": "Qmf9kBpHUhQwzRV43ChazPudR82N3wyTDh4rZsWj2UAma2"
|
||||
"23CE3C174789CDD306497D86CB2C4E76BA8B06E5": {
|
||||
"logo": "Qmdwyb2mL1zDo1BMnNVFTGBgsrzZZjmjWeop9pcfP88zhK",
|
||||
"name": "Golf (USA)",
|
||||
"vid": "QmVvzYoiK3xesgZveFqRhmQ14v11jcdAsTQj9rTf5DyW7v"
|
||||
},
|
||||
"F5057FA9BFD9D60B9DCFC004CFDD12AA8EB1CB4A": {
|
||||
"name": "Red Alarm (Japan)"
|
||||
"266FE615EE3DF1160A20B456824699F16810FA28": {
|
||||
"logo": "QmXNUHwFo4822jWfXpwutWJTsdLu5JasjiAT1aPNkvqT56",
|
||||
"name": "Virtual League Baseball (USA)",
|
||||
"vid": "QmZXfBoZfHppqtL4XwrPLALomir7ZsdQ7GehvkB6y2TwRP"
|
||||
},
|
||||
"494FA60CB8997880B4E8CD3270B21D6C2768A48A": {
|
||||
"name": "Red Alarm (USA)",
|
||||
"vid": "QmRidMNd9q92SawBCir2rW6EJsjm7SRjUpgkXmUTUCBGCV",
|
||||
"logo": "Qmcz6tW9nEPT5Vp4KYvBw5nspXfo6M5e2WtSuKoThEsZiE"
|
||||
"274C328FBD904F20E69172AB826BF8F94CED1BDB": {
|
||||
"logo": "QmNvnaDejYEdsowxN8KZaj6qdj4vqVv8btBy4pSxdyJHNW",
|
||||
"name": "Virtual Boy Wario Land (Japan, USA)",
|
||||
"vid": "QmYxxf3mjbPhA8JKkn8yZh73LC1UHCdWrtviMvjTg8VqRe"
|
||||
},
|
||||
"1A67F6BDB45DB46E22B8AADEA754A68AD379AE29": {
|
||||
"name": "SD Gundam - Dimension War (Japan)",
|
||||
"vid": "QmR2dDWQ3GrT842baDxNQHP6N6wmfheZGZ2fqYWHRK2yYF",
|
||||
"logo": "QmPoFytQpiAoJ9nW4ZrjjJMVdAcZyME7SRRVDSnK5dMkPc"
|
||||
"322DC4C2DF74BF19D8A30D43C489401BE5E76CA5": {
|
||||
"name": "Hyper Fighting (World) (Beta) (Aftermarket) (Unl) (Pirate)",
|
||||
"ref": "578355DE83822CE67522033DD7545631124A71AB"
|
||||
},
|
||||
"16AD904FAAFD24642DFD056D10A768E81F0D9BFA": {
|
||||
"name": "Space Invaders - Virtual Collection (Japan)",
|
||||
"vid": "QmUe2GCnx99s9DZxaQtHmtNrzRJ7YvHGi1NuqiAtDf46cj",
|
||||
"logo": "QmUpBKPjXguAkS69F1ZQRoX5D3kRznYoEVYF8bFqLpBA1R"
|
||||
"37E06E53CE2810E870835C48DECDD6C72079BFDA": {
|
||||
"logo": "QmV2hr6f1Sf2ZkfmtjYXDPTRjXyQ3qa3XvmxHqmYcUnLMP",
|
||||
"name": "Innsmouth no Yakata (Japan)",
|
||||
"vid": "QmS7VL589BMEasXVM3tBDwfEjqUpcATcMP4GN4RMauh5yw"
|
||||
},
|
||||
"DC182360C7C8D26323DB8921F09DB76638E81CED": {
|
||||
"name": "Space Pinball (Japan) (En) (Proto)"
|
||||
"38F9008F60B09DEEF1151D46B905B090A0338200": {
|
||||
"logo": "QmSYrvARB5C2QFF1MjTjGnrr3Vwh42iXXd9uXFKbYHF1tm",
|
||||
"name": "Vertical Force (USA)",
|
||||
"vid": "QmPpDpBSRP8UZidJLsJiRaWoFXY1M5WKnU2ZApzSzshwJB"
|
||||
},
|
||||
"3F08CAE2B6F9E386529949FF954220860DB1AF64": {
|
||||
"name": "Space Squash (Japan)"
|
||||
},
|
||||
"494FA60CB8997880B4E8CD3270B21D6C2768A48A": {
|
||||
"logo": "Qmcz6tW9nEPT5Vp4KYvBw5nspXfo6M5e2WtSuKoThEsZiE",
|
||||
"name": "Red Alarm (USA)",
|
||||
"vid": "QmRidMNd9q92SawBCir2rW6EJsjm7SRjUpgkXmUTUCBGCV"
|
||||
},
|
||||
"5162F7FA9E3EAE4338C53CCBA641BA080C768754": {
|
||||
"logo": "QmeeJjkBUR8vXjvxo8xwixChLpeNShxiWT9wjeaafNu54x",
|
||||
"name": "Mario's Tennis (Japan, USA)",
|
||||
"vid": "QmV46x6EkJUYw69Li1KC2yGuDWKcoPVRAvXoUZGfr58Sjq"
|
||||
},
|
||||
"5177015A91442E56BD76AF39447BCA365E06C272": {
|
||||
"logo": "QmYB88E8a8SfCrFhJ7jaEkU6arv4prof96iMbKQSUrD5Ww",
|
||||
"name": "3-D Tetris (USA)",
|
||||
"vid": "QmcsARDkyozauAacURMi33H5TXAtraTWGJzPmJv2onvGQJ"
|
||||
},
|
||||
"578355DE83822CE67522033DD7545631124A71AB": {
|
||||
"logo": "QmQPUUg8cHNUmbwChaYV8ejbdoo8kW3G6smRsauUVKectJ",
|
||||
"name": "Hyper Fighting (World) (Aftermarket) (Unl) (Pirate)",
|
||||
"vid": "QmfBs2QTLxhsN5LtpVxW4NUUWp9qEn5o69xXKc8Qd4cCHg"
|
||||
},
|
||||
"583B409B7215159219D08E789DB46140062095F2": {
|
||||
"name": "Virtual Fishing (Japan)"
|
||||
},
|
||||
"7556A778B60490BDB81774BCBAA7413FC84CB985": {
|
||||
"logo": "QmXiFK6k798oYjYPYpG4zZ3wc6YnhcmrM2Mw4XHNxXyyXr",
|
||||
"name": "Mario Clash (Japan, USA)",
|
||||
"vid": "QmXMujzzbxGLUNjVeFY9rexf8F2HuVyUR53tM3dLmHw6Up"
|
||||
},
|
||||
"80216B2874CF162F301D571BB8AEBC98D80B4F3E": {
|
||||
"logo": "Qmf9kBpHUhQwzRV43ChazPudR82N3wyTDh4rZsWj2UAma2",
|
||||
"name": "Panic Bomber (USA)",
|
||||
"vid": "QmS46otKp9pMfQFz147Qdp7QRq9LF4k1wHKU7nFq9pKc3F"
|
||||
},
|
||||
"8BB91E681959207BE068796D540120565C174D37": {
|
||||
"ref": "37E06E53CE2810E870835C48DECDD6C72079BFDA"
|
||||
},
|
||||
"93A25ABF4F86E71A49C1C3A470140BB40CB693D6": {
|
||||
"logo": "QmfPLMFW7RHrNQbZKj6kME6fA3p1rQJvvPzCdMYVmkf5Bf",
|
||||
"name": "Galactic Pinball (Japan, USA)",
|
||||
"vid": "QmcZ6zTS3VPzSMRjTxQUkD5zeRssNosfL1BKbPvbHeRzXi"
|
||||
},
|
||||
"A5BE7654037050F0A781E70EFEA0191F43D26F06": {
|
||||
"logo": "QmRMLBV9VMwafg6FEE2EioGGmAZiDvnCwcjk89aPzBbZKc",
|
||||
"name": "Virtual Bowling (Japan)",
|
||||
"vid": "Qmcys2TXepzdxM1pr3TuFybyCSqPtqErTHP8XNqiRzDHiC"
|
||||
},
|
||||
"A973406590382EE503037271330D68D2834B79DB": {
|
||||
"name": "Jack Bros. no Meiro de Hiihoo! (Japan)"
|
||||
},
|
||||
"AB8FA82B79512EEFEFCCDCCEA6768078A374C4AA": {
|
||||
"name": "Virtual Pro Yakyuu '95 (Japan)"
|
||||
},
|
||||
"B8A12A9677AFA5CBCE6ED43EB0EFFB5A02875534": {
|
||||
"name": "Tobidase! Panibon (Japan)"
|
||||
},
|
||||
"C595285D42C69F14B2B418C1EDFBE4A7F9A624B6": {
|
||||
"name": "T&E Virtual Golf (Japan)"
|
||||
},
|
||||
"C59E020F9674774C5CBC022317EBBA0EB1D744F7": {
|
||||
"logo": "QmfUGi2NxBeuL2zQJfKsXxxVgYKZDkxMS4uyunb5Ssbtdg",
|
||||
"name": "Teleroboxer (Japan, USA)",
|
||||
"vid": "QmYyjbruy62ZupgBpJswyj6M41FhL6PWpx88P5TjYcWga1",
|
||||
"logo": "QmfUGi2NxBeuL2zQJfKsXxxVgYKZDkxMS4uyunb5Ssbtdg"
|
||||
},
|
||||
"B8A12A9677AFA5CBCE6ED43EB0EFFB5A02875534": {
|
||||
"name": "Tobidase! Panibon (Japan)"
|
||||
},
|
||||
"FF7D4DFC467E6E0D3FE8517102132A65A9D17589": {
|
||||
"name": "V-Tetris (Japan)",
|
||||
"vid": "QmZEKGFQkgeQzPkuShnx456pqcf94oqus3Fv2SyHzKMjiq",
|
||||
"logo": "QmcBz5NZZN6UuKVDvFMjnxFfvfsCjxZvWXFjMLKJunMXqg"
|
||||
},
|
||||
"38F9008F60B09DEEF1151D46B905B090A0338200": {
|
||||
"name": "Vertical Force (USA)",
|
||||
"vid": "QmPpDpBSRP8UZidJLsJiRaWoFXY1M5WKnU2ZApzSzshwJB",
|
||||
"logo": "QmSYrvARB5C2QFF1MjTjGnrr3Vwh42iXXd9uXFKbYHF1tm"
|
||||
"vid": "QmYyjbruy62ZupgBpJswyj6M41FhL6PWpx88P5TjYcWga1"
|
||||
},
|
||||
"C7204BA5CFE7D26394B5E22BADC580C8ED8C0B37": {
|
||||
"name": "Vertical Force (Japan)"
|
||||
},
|
||||
"A5BE7654037050F0A781E70EFEA0191F43D26F06": {
|
||||
"name": "Virtual Bowling (Japan)",
|
||||
"vid": "Qmcys2TXepzdxM1pr3TuFybyCSqPtqErTHP8XNqiRzDHiC",
|
||||
"logo": "QmRMLBV9VMwafg6FEE2EioGGmAZiDvnCwcjk89aPzBbZKc"
|
||||
},
|
||||
"274C328FBD904F20E69172AB826BF8F94CED1BDB": {
|
||||
"name": "Virtual Boy Wario Land (Japan, USA)",
|
||||
"vid": "QmYxxf3mjbPhA8JKkn8yZh73LC1UHCdWrtviMvjTg8VqRe",
|
||||
"logo": "QmNvnaDejYEdsowxN8KZaj6qdj4vqVv8btBy4pSxdyJHNW"
|
||||
},
|
||||
"583B409B7215159219D08E789DB46140062095F2": {
|
||||
"name": "Virtual Fishing (Japan)"
|
||||
"name": "Vertical Force (Japan)",
|
||||
"ref": "38F9008F60B09DEEF1151D46B905B090A0338200"
|
||||
},
|
||||
"D96C9F8AAC5B4EA012A8FE659BB68FB1159A9C6D": {
|
||||
"logo": "QmZ57YMihbyobJp947F4gKUivX5EnGcpmSn1gQiCLwSn5y",
|
||||
"name": "Virtual Lab (Japan)",
|
||||
"vid": "QmTJM6eCADPf7zacHNzchYeQ9SRaAepeqX8NmrtUX1PSAf",
|
||||
"logo": "QmZ57YMihbyobJp947F4gKUivX5EnGcpmSn1gQiCLwSn5y"
|
||||
"vid": "QmTJM6eCADPf7zacHNzchYeQ9SRaAepeqX8NmrtUX1PSAf"
|
||||
},
|
||||
"266FE615EE3DF1160A20B456824699F16810FA28": {
|
||||
"name": "Virtual League Baseball (USA)",
|
||||
"vid": "QmZXfBoZfHppqtL4XwrPLALomir7ZsdQ7GehvkB6y2TwRP",
|
||||
"logo": "QmXNUHwFo4822jWfXpwutWJTsdLu5JasjiAT1aPNkvqT56"
|
||||
},
|
||||
"AB8FA82B79512EEFEFCCDCCEA6768078A374C4AA": {
|
||||
"name": "Virtual Pro Yakyuu '95 (Japan)"
|
||||
"DC182360C7C8D26323DB8921F09DB76638E81CED": {
|
||||
"name": "Space Pinball (Japan) (En) (Proto)"
|
||||
},
|
||||
"DCC46484BD0ACAB0AC1EA178F425A0F5CCFB8DC2": {
|
||||
"logo": "QmdU2xf9SKUxijX6tWzKrGx8nTNhmPh9V1EhNkQFYXYfwJ",
|
||||
"name": "Waterworld (USA)",
|
||||
"vid": "QmXksqwSJtyVEWQScPRbiEcsDw1vZgrwjD1sbPzPzZHCmh",
|
||||
"logo": "QmdU2xf9SKUxijX6tWzKrGx8nTNhmPh9V1EhNkQFYXYfwJ"
|
||||
"vid": "QmXksqwSJtyVEWQScPRbiEcsDw1vZgrwjD1sbPzPzZHCmh"
|
||||
},
|
||||
"F4A4C7928F15102CF14C90C5F044E5F7CC7C32F9": {
|
||||
"logo": "QmYhPDmeG8cj88j2JMyvtgRqXD6Jj5ee4g55mfRAphVeUS",
|
||||
"name": "Nester's Funky Bowling (USA)",
|
||||
"vid": "Qmd115CebnftLvbWYyiNdBXSgj45tHPqezmqgZF4EaHhkp"
|
||||
},
|
||||
"F5057FA9BFD9D60B9DCFC004CFDD12AA8EB1CB4A": {
|
||||
"name": "Red Alarm (Japan)",
|
||||
"ref": "494FA60CB8997880B4E8CD3270B21D6C2768A48A"
|
||||
},
|
||||
"FF7D4DFC467E6E0D3FE8517102132A65A9D17589": {
|
||||
"logo": "QmcBz5NZZN6UuKVDvFMjnxFfvfsCjxZvWXFjMLKJunMXqg",
|
||||
"name": "V-Tetris (Japan)",
|
||||
"vid": "QmZEKGFQkgeQzPkuShnx456pqcf94oqus3Fv2SyHzKMjiq"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,206 +1,209 @@
|
||||
{
|
||||
"65D07426B520DDD3115D40F255511E0FD2E20AE7": {
|
||||
"name": "[BIOS] GCE Vectrex (World)"
|
||||
},
|
||||
"4A833E4869221F832DACB7EF0FB2997C870B7D7F": {
|
||||
"name": "3D Crazy Coaster (USA)"
|
||||
},
|
||||
"1731E892DA8945FE4945B43847D23960C581682A": {
|
||||
"name": "3D Mine Storm (USA)"
|
||||
},
|
||||
"9E5D23CB7EF111394E1804E105B6DD277C062DF6": {
|
||||
"name": "3D Narrow Escape (USA)"
|
||||
},
|
||||
"71946BE47DEBAE89AD191AC382C2015D6A862072": {
|
||||
"name": "AnimAction - Advanced Animation (USA)"
|
||||
},
|
||||
"EB76F860130F8CC87826FA931350467760511213": {
|
||||
"name": "Armor..Attack (World)",
|
||||
"vid": "QmNjJ6K9gL5U3N4atFXs3FBeuaZNJKXuPMcNkU2ALVfcC8",
|
||||
"logo": "QmTaFSMSwyHEmbsJiQz5Fopa3pJJJFoGViVARw5fhDH3db"
|
||||
},
|
||||
"8F35BAC5E8E0FC4AD7291BBFE5AE6E693272DE06": {
|
||||
"name": "Art Master (USA)"
|
||||
},
|
||||
"3FF3FA4FEDA6D562DEDFF2A10CFA8679D8318044": {
|
||||
"name": "Bedlam (USA, Europe)",
|
||||
"vid": "QmXQbibFnpksG3DqupoXs9gBzexvwC3gW1zdcYrX9oSwa9",
|
||||
"logo": "QmNNDtCUNmUzDH2TYkFCTojijTaUgfRVNd72XpZ1exV2fx"
|
||||
},
|
||||
"E4BADD494ED548721328EF869D33AD70E3E522C9": {
|
||||
"name": "Berzerk (World)",
|
||||
"vid": "QmTBGNBX64jyNVhW3QmJyXWzrXMayNunhg7cZ5ixYCi5Ew",
|
||||
"logo": "Qmb3D82JxKqf7KFHXGSbTR5cBojXbZYUjwRqhpV8NPUPwn"
|
||||
},
|
||||
"AA1AA811C9CE5E19F43B78A6531CDD0E707337BD": {
|
||||
"name": "Blitz! - Action Football (USA, Europe) (881B27C5)"
|
||||
},
|
||||
"60893DFADA42CF50C94083D45AD5BD14D0C031C8": {
|
||||
"name": "Blitz! - Action Football (USA, Europe) (0F11CE0C)",
|
||||
"vid": "QmebzT27GkPGZeMzQk4Go4MdDV8LpMZAhMgJVPASTtGejB",
|
||||
"logo": "QmTnugssuLSRDNiRLjcV8bFLfpixsbZW8nhpfPpMHQtZmA"
|
||||
},
|
||||
"67F8513958C04E936B135740ED4EC6E6FA1763D5": {
|
||||
"name": "Clean Sweep (World)",
|
||||
"vid": "QmTvzqndwrtA8onrSazzqZ9hqpzc32z2G4UFA3BEagLmvK",
|
||||
"logo": "QmYRFHuN1Pr6VZ1VyTSNzoCBzXSXUWNhWbsrrsTYWuGFDX"
|
||||
},
|
||||
"56A7F277BF09F94461DDA542C04C36266D838B86": {
|
||||
"name": "Cosmic Chasm (World)",
|
||||
"vid": "QmUju26mxReyJZrjkbNWPwyeWLeKcZVGSdsvaqu49L4hVj",
|
||||
"logo": "QmUzBST6yYnmLp7eTjhARck67Sa2MdH4Pzxnr8LLWBK1wG"
|
||||
},
|
||||
"DBB0B378AACC2A18DFB476AA99AB00D6AB395205": {
|
||||
"name": "Dark Tower (USA) (Proto)",
|
||||
"vid": "QmdgHceUf86DcvZYmZTWhMZVj6WMWELC8ugFB6DgE3U4T5",
|
||||
"logo": "QmdQZhuLH5nzGTSX9rw6Co6dsTMySmurQ2kWXSesmCvfYw"
|
||||
},
|
||||
"C226D78301DBE40F2194C165F2124D5AFA885BE4": {
|
||||
"name": "Engine Analyzer (USA) (Proto)"
|
||||
},
|
||||
"286E061331289B031DE33725A4966C379C1DF47F": {
|
||||
"name": "Fortress of Narzod (USA, Europe)",
|
||||
"vid": "QmQd5CNoL4FNqqr4Euw9kK6mNkzibGmTXZ8YeAocxMLTsX",
|
||||
"logo": "QmaSxgcqfeZz3KvgTiK9uWqm6zdw25i8Zw6khLS35FzzNd"
|
||||
},
|
||||
"0406C13952FB0BD6ADC68298AE163EBC6160484C": {
|
||||
"logo": "QmPM6vguoNJ2jf7ZDVLiv9ZPbTxSP7SGSgu5c5ZutkGW4L",
|
||||
"name": "Heads-Up - Action Soccer (USA)",
|
||||
"vid": "QmSeDTnrAZsZdS9X3EZGycQiWHmnCyYFhmsrbobu2oVUdt",
|
||||
"logo": "QmPM6vguoNJ2jf7ZDVLiv9ZPbTxSP7SGSgu5c5ZutkGW4L"
|
||||
},
|
||||
"A246F1D94A5B062B60784787A1D6D499B21C10CD": {
|
||||
"name": "HyperChase - Auto Race (World)",
|
||||
"vid": "Qmafws3VndL7PSxxzZLx6KpLL2Nf8NxZU2NZZPDgNqnxBj",
|
||||
"logo": "QmXfyfqTdJMRN5j5heiiqkiicQFyppQFPpPhNR7mEZcxLn"
|
||||
"vid": "QmSeDTnrAZsZdS9X3EZGycQiWHmnCyYFhmsrbobu2oVUdt"
|
||||
},
|
||||
"05F56E708F752CA801F8013C7DA9B0DB01508BA5": {
|
||||
"name": "Mail Plane (USA) (Proto) (DA1AC0DB)"
|
||||
},
|
||||
"969D5EC7D7ABA9AC25E54069149D9586217AB63D": {
|
||||
"name": "Mail Plane (USA) (Proto) (05838962)"
|
||||
"08E2DF9F7769C16A4D730ADF8D659F8EA4BE7F9D": {
|
||||
"name": "WebWars (USA)"
|
||||
},
|
||||
"0C229DD12A55044681585F7FDABD37DBC1243B59": {
|
||||
"logo": "QmWy88c5QpxqpFXa36Z5hSRjRuZ5tTJsM61ypyCdePaKK2",
|
||||
"name": "Space Wars (World)",
|
||||
"vid": "QmTFQHny9TGVGTLf4iMqjp92izzxLVB7AEefntitV6gQxr"
|
||||
},
|
||||
"1731E892DA8945FE4945B43847D23960C581682A": {
|
||||
"name": "3D Mine Storm (USA)"
|
||||
},
|
||||
"1FDCC6E54AE5177BC9CDC79CE616AE3401E5C229": {
|
||||
"name": "Mine Storm II (USA) (Rev 2)"
|
||||
},
|
||||
"22F7A490F6F7AAA31E892B03A85178AF211F62E3": {
|
||||
"logo": "QmdH43xxRiRcH1QH1eK54F6VPsMru9VFZCjyetGqhRnS2m",
|
||||
"name": "Rip Off (World)",
|
||||
"vid": "QmUkKdrEFZ6pNQXqMjvDS1xav4CZoU8NQWZCeLcSzjQoLB"
|
||||
},
|
||||
"286E061331289B031DE33725A4966C379C1DF47F": {
|
||||
"logo": "QmaSxgcqfeZz3KvgTiK9uWqm6zdw25i8Zw6khLS35FzzNd",
|
||||
"name": "Fortress of Narzod (USA, Europe)",
|
||||
"vid": "QmQd5CNoL4FNqqr4Euw9kK6mNkzibGmTXZ8YeAocxMLTsX"
|
||||
},
|
||||
"299841A116E59399AF4E74CCAE23DDFC078E007B": {
|
||||
"name": "Mr. Boston Clean Sweep (Europe)"
|
||||
},
|
||||
"2E8285061530CCADFA2D8D11C374AE244449EBFA": {
|
||||
"logo": "QmRtXFBb1LxYizttvoTquUdnxxmMbBpDgCJbPNF1LimAXy",
|
||||
"name": "Spin ball (USA)",
|
||||
"vid": "QmdbR1gfqid1H4iSUDtLbUWL6rRFq8aW39k1DHULcAP4C6"
|
||||
},
|
||||
"38072DC491D5C729C06F4C50BC310D361E0FF62A": {
|
||||
"logo": "QmRbeyGtPXFNa8Yd7pvgSHCWHQhimdSbzUcajSjuFkpoRM",
|
||||
"name": "Solar Quest (World)",
|
||||
"vid": "QmeafNyYnAWfgdavXZbdXopzgvsYjzPnhktM52ko1gUmq9"
|
||||
},
|
||||
"38E38B5C60466146D4648F8929B5CE3A08DCBE0D": {
|
||||
"logo": "QmaXvh9s38tXrrdZpLJcisAdCmGKJZsAqs23uRrcGa4ZMw",
|
||||
"name": "Scramble (USA, Europe)",
|
||||
"vid": "QmQ7urxD1C6p4WzXPABtLbkokCNFQ9RZmeTXdvofRQbkA8"
|
||||
},
|
||||
"3A2EE61316FB316D9E9EFDF5EE405E813475B466": {
|
||||
"logo": "QmVtydkMEKK3MNFJM9aVcppUPLSVtZ3Nrq7JM2NrDvBSYj",
|
||||
"name": "Star Trek - The Motion Picture (USA)",
|
||||
"vid": "QmX3x5Ftz7N9XEn48EdwdcXUKcankDLULGMFdhoEKSuCEA"
|
||||
},
|
||||
"3FF3FA4FEDA6D562DEDFF2A10CFA8679D8318044": {
|
||||
"logo": "QmNNDtCUNmUzDH2TYkFCTojijTaUgfRVNd72XpZ1exV2fx",
|
||||
"name": "Bedlam (USA, Europe)",
|
||||
"vid": "QmXQbibFnpksG3DqupoXs9gBzexvwC3gW1zdcYrX9oSwa9"
|
||||
},
|
||||
"4A833E4869221F832DACB7EF0FB2997C870B7D7F": {
|
||||
"name": "3D Crazy Coaster (USA)"
|
||||
},
|
||||
"4B1E43808AEFC8235B130D2DBAC41FA087D9AEDF": {
|
||||
"logo": "QmcwcZax6zKfh1hELpA7v7vWSrRTA6P4GcbvGNWXDY2bVi",
|
||||
"name": "Vectrex Pong (1998) (PD)",
|
||||
"vid": "QmPPCkKNMtttNQx7rc6c773PvHz8CdrvKtPFbsoMYsDvph"
|
||||
},
|
||||
"4F461B0B51C82433E70130EAC5494F26239B4453": {
|
||||
"logo": "QmTxYL5HLGHDJqqUAEhWZpEMAYhdAYg4CwebGy2cddJoxD",
|
||||
"name": "Tour De France (USA) (Proto)",
|
||||
"vid": "QmXQ2KdKwYS7b4zAfgWQXxUo6jhQPKjDfYrmNzS2BtL72B"
|
||||
},
|
||||
"526B347BC02A47C8BDA326B1F6DA50284127F038": {
|
||||
"name": "Sledge 3D (USA) (Sample)"
|
||||
},
|
||||
"56A7F277BF09F94461DDA542C04C36266D838B86": {
|
||||
"logo": "QmUzBST6yYnmLp7eTjhARck67Sa2MdH4Pzxnr8LLWBK1wG",
|
||||
"name": "Cosmic Chasm (World)",
|
||||
"vid": "QmUju26mxReyJZrjkbNWPwyeWLeKcZVGSdsvaqu49L4hVj"
|
||||
},
|
||||
"56CEA447704286D88533DB99FD209816AE1B96A4": {
|
||||
"logo": "Qmf5keLWEGScQ3q8WV7BC5yYmfVLMfbHz6odtUWGQNVQfL",
|
||||
"name": "Star Wars Theme by Chris Salomon (1998) (PD)",
|
||||
"vid": "QmPSj39boR1gkrVfKh38iEKx6GpKwNTS851oCbYeFXCrQh"
|
||||
},
|
||||
"58AECC7FB1D26A22B09E9A13D845E4ED12E2449D": {
|
||||
"name": "Melody Master - Music Composition and Entertainment (USA)"
|
||||
},
|
||||
"5F00ED35F189357B4A16A190520166371CA57906": {
|
||||
"logo": "QmX4wEb2TkewvTSAz6L6Ezdwx8SJeWPw1j3Npmjwm9AFYR",
|
||||
"name": "Mine Storm (World)",
|
||||
"vid": "QmQfbMcJPPjnd1jihNsXxdvyWRqvBhhWFYH9yui8A2Sohf",
|
||||
"logo": "QmX4wEb2TkewvTSAz6L6Ezdwx8SJeWPw1j3Npmjwm9AFYR"
|
||||
"vid": "QmQfbMcJPPjnd1jihNsXxdvyWRqvBhhWFYH9yui8A2Sohf"
|
||||
},
|
||||
"1FDCC6E54AE5177BC9CDC79CE616AE3401E5C229": {
|
||||
"name": "Mine Storm II (USA) (Rev 2)"
|
||||
"60893DFADA42CF50C94083D45AD5BD14D0C031C8": {
|
||||
"logo": "QmTnugssuLSRDNiRLjcV8bFLfpixsbZW8nhpfPpMHQtZmA",
|
||||
"name": "Blitz! - Action Football (USA, Europe) (0F11CE0C)",
|
||||
"vid": "QmebzT27GkPGZeMzQk4Go4MdDV8LpMZAhMgJVPASTtGejB"
|
||||
},
|
||||
"299841A116E59399AF4E74CCAE23DDFC078E007B": {
|
||||
"name": "Mr. Boston Clean Sweep (Europe)"
|
||||
"65D07426B520DDD3115D40F255511E0FD2E20AE7": {
|
||||
"name": "[BIOS] GCE Vectrex (World)"
|
||||
},
|
||||
"F6F26BEB20BB364761A9466FF4087C7B9713D048": {
|
||||
"name": "Pitcher's Duel (USA) (Proto)"
|
||||
"67F8513958C04E936B135740ED4EC6E6FA1763D5": {
|
||||
"logo": "QmYRFHuN1Pr6VZ1VyTSNzoCBzXSXUWNhWbsrrsTYWuGFDX",
|
||||
"name": "Clean Sweep (World)",
|
||||
"vid": "QmTvzqndwrtA8onrSazzqZ9hqpzc32z2G4UFA3BEagLmvK"
|
||||
},
|
||||
"87248DCD3DB51C1EBF3F8E07E21EC021EEC0B9BE": {
|
||||
"name": "Polar Rescue (USA)",
|
||||
"vid": "QmNmQyQSUy4kf2nbsqbQ9zTWw5VLSNCXDK3GnL8WhvNJVj",
|
||||
"logo": "QmRVhA3yZJbSdkD1evpnpKHutDxdwAtEpWGJwF4fDCV3JQ"
|
||||
},
|
||||
"B7BD8C8003C60535E1E776758F5C289AA9A77E81": {
|
||||
"name": "Polar Rescue (USA) (Beta)"
|
||||
},
|
||||
"9F5B0B5E6CAF6D887D3637ACE70AD6A6258B999D": {
|
||||
"name": "Pole Position (USA) (C10F37D8)"
|
||||
},
|
||||
"8454D472435E7A6A4CA5FE129162480CA6E84B87": {
|
||||
"name": "Pole Position (USA) (A00ED3D6)",
|
||||
"vid": "QmUSws3tEN8qcFUCcS7vegLDs4gRwsudDcD8PAE94vHkeu",
|
||||
"logo": "QmdKNUFPsB3qUDzNniq1Z5UNgCd6NkeucAdr2y7H2FmnJF"
|
||||
},
|
||||
"7CE85DB8DD32687AD7629631AE113820371FAF7C": {
|
||||
"name": "RA+A Spectrum I+ Stress Tester (USA) (Proto)"
|
||||
},
|
||||
"22F7A490F6F7AAA31E892B03A85178AF211F62E3": {
|
||||
"name": "Rip Off (World)",
|
||||
"vid": "QmUkKdrEFZ6pNQXqMjvDS1xav4CZoU8NQWZCeLcSzjQoLB",
|
||||
"logo": "QmdH43xxRiRcH1QH1eK54F6VPsMru9VFZCjyetGqhRnS2m"
|
||||
},
|
||||
"38E38B5C60466146D4648F8929B5CE3A08DCBE0D": {
|
||||
"name": "Scramble (USA, Europe)",
|
||||
"vid": "QmQ7urxD1C6p4WzXPABtLbkokCNFQ9RZmeTXdvofRQbkA8",
|
||||
"logo": "QmaXvh9s38tXrrdZpLJcisAdCmGKJZsAqs23uRrcGa4ZMw"
|
||||
},
|
||||
"526B347BC02A47C8BDA326B1F6DA50284127F038": {
|
||||
"name": "Sledge 3D (USA) (Sample)"
|
||||
},
|
||||
"38072DC491D5C729C06F4C50BC310D361E0FF62A": {
|
||||
"name": "Solar Quest (World)",
|
||||
"vid": "QmeafNyYnAWfgdavXZbdXopzgvsYjzPnhktM52ko1gUmq9",
|
||||
"logo": "QmRbeyGtPXFNa8Yd7pvgSHCWHQhimdSbzUcajSjuFkpoRM"
|
||||
},
|
||||
"0C229DD12A55044681585F7FDABD37DBC1243B59": {
|
||||
"name": "Space Wars (World)",
|
||||
"vid": "QmTFQHny9TGVGTLf4iMqjp92izzxLVB7AEefntitV6gQxr",
|
||||
"logo": "QmWy88c5QpxqpFXa36Z5hSRjRuZ5tTJsM61ypyCdePaKK2"
|
||||
},
|
||||
"8BBA6705E6F69F55AB6D105EAC2FD5B0C0D16D9F": {
|
||||
"name": "Spike (USA, Europe)",
|
||||
"vid": "QmNP7rHCqhNAPNYAhYoSke21dEGJdmY4HdX6CgD1wX2ScZ",
|
||||
"logo": "QmNhbeQnhDomVvUjN4g98dNQgnpGfnHjdxhnZH2vy11WtB"
|
||||
},
|
||||
"2E8285061530CCADFA2D8D11C374AE244449EBFA": {
|
||||
"name": "Spin ball (USA)",
|
||||
"vid": "QmdbR1gfqid1H4iSUDtLbUWL6rRFq8aW39k1DHULcAP4C6",
|
||||
"logo": "QmRtXFBb1LxYizttvoTquUdnxxmMbBpDgCJbPNF1LimAXy"
|
||||
},
|
||||
"C9B79F1BA30DCBCE5A068B606A97775CF863FE0A": {
|
||||
"name": "Star Castle (USA)",
|
||||
"vid": "QmUFK8JWdwMnToSanY6mKcPPCURXepY71ug4dkSgeWFkip",
|
||||
"logo": "QmZTwQkihBa2VeUgHKGVAJnQ2o1x8cL7sLqtWR8AizhWBL"
|
||||
},
|
||||
"D182EAC5A54FFC642FCBE4A495C7C09F87B43585": {
|
||||
"name": "Star Ship (Europe)",
|
||||
"vid": "QmfA9z3dvG6HRtyzKgChGSLvqVFMGFGSzREwbKE9JuDim2",
|
||||
"logo": "QmTnYiP8VBd3tXtJKhRpM1d3fmeeUBDTB1H3NTmav7hwNN"
|
||||
},
|
||||
"3A2EE61316FB316D9E9EFDF5EE405E813475B466": {
|
||||
"name": "Star Trek - The Motion Picture (USA)",
|
||||
"vid": "QmX3x5Ftz7N9XEn48EdwdcXUKcankDLULGMFdhoEKSuCEA",
|
||||
"logo": "QmVtydkMEKK3MNFJM9aVcppUPLSVtZ3Nrq7JM2NrDvBSYj"
|
||||
},
|
||||
"D6B388B3951E34971C1B00C7FDF8DD00AD52DF09": {
|
||||
"name": "StarHawk (World)",
|
||||
"vid": "QmTkv8Lsq78Ht3TKP89Jjor3nR4MhT245MMDtxa3jGsBmb",
|
||||
"logo": "QmZYtvLf6opgdsBMUUic88kGmdjz1iSE5prHwQvjDVYEa5"
|
||||
},
|
||||
"E3418055F1E79A2300572C557E71BF2CE8BADBFA": {
|
||||
"name": "Test Cartridge (USA) (Rev 4) (Proto)"
|
||||
},
|
||||
"4F461B0B51C82433E70130EAC5494F26239B4453": {
|
||||
"name": "Tour De France (USA) (Proto)",
|
||||
"vid": "QmXQ2KdKwYS7b4zAfgWQXxUo6jhQPKjDfYrmNzS2BtL72B",
|
||||
"logo": "QmTxYL5HLGHDJqqUAEhWZpEMAYhdAYg4CwebGy2cddJoxD"
|
||||
},
|
||||
"9681DC5D38A31CB0D0C6C3BD0FC76BFDDE531CCE": {
|
||||
"name": "WebWarp (Europe)",
|
||||
"vid": "QmZ6wtHLWez5EWXpK9DLKW4Bbwn2MHAJqmzNneb93S97kn",
|
||||
"logo": "QmQmenQKUtDTuzGcrXCFWQ5mddcJGnXfCdk2g2KvSikT3A"
|
||||
},
|
||||
"08E2DF9F7769C16A4D730ADF8D659F8EA4BE7F9D": {
|
||||
"name": "WebWars (USA)"
|
||||
},
|
||||
"56CEA447704286D88533DB99FD209816AE1B96A4": {
|
||||
"name": "Star Wars Theme by Chris Salomon (1998) (PD)",
|
||||
"vid": "QmPSj39boR1gkrVfKh38iEKx6GpKwNTS851oCbYeFXCrQh",
|
||||
"logo": "Qmf5keLWEGScQ3q8WV7BC5yYmfVLMfbHz6odtUWGQNVQfL"
|
||||
},
|
||||
"83A6269A9275D8CE48C58B5560FB2D7324EAEEA0": {
|
||||
"name": "Vector Vaders by John Dondzila (1996)",
|
||||
"vid": "QmSyQ7zP7FZEPLHQvHdUWe5YDy73NFhrpHG1vtZL8zxzjh",
|
||||
"logo": "QmahNM3mUSdGTSHWu146L7hf5HZsYvMPRUJyQn1w7pJ2zr"
|
||||
},
|
||||
"4B1E43808AEFC8235B130D2DBAC41FA087D9AEDF": {
|
||||
"name": "Vectrex Pong (1998) (PD)",
|
||||
"vid": "QmPPCkKNMtttNQx7rc6c773PvHz8CdrvKtPFbsoMYsDvph",
|
||||
"logo": "QmcwcZax6zKfh1hELpA7v7vWSrRTA6P4GcbvGNWXDY2bVi"
|
||||
"71946BE47DEBAE89AD191AC382C2015D6A862072": {
|
||||
"name": "AnimAction - Advanced Animation (USA)"
|
||||
},
|
||||
"745DDBDC285F1DA4F7C3EB5E44FE252D503C7A15": {
|
||||
"name": "Wormhole by John Dondzila (2001) (PD)",
|
||||
"vid": "Qmd6pfnvEjTddPaZQddxNNiA3rkHQVshbtZekKqfmfTdBg"
|
||||
},
|
||||
"7CE85DB8DD32687AD7629631AE113820371FAF7C": {
|
||||
"name": "RA+A Spectrum I+ Stress Tester (USA) (Proto)"
|
||||
},
|
||||
"83A6269A9275D8CE48C58B5560FB2D7324EAEEA0": {
|
||||
"logo": "QmahNM3mUSdGTSHWu146L7hf5HZsYvMPRUJyQn1w7pJ2zr",
|
||||
"name": "Vector Vaders by John Dondzila (1996)",
|
||||
"vid": "QmSyQ7zP7FZEPLHQvHdUWe5YDy73NFhrpHG1vtZL8zxzjh"
|
||||
},
|
||||
"8454D472435E7A6A4CA5FE129162480CA6E84B87": {
|
||||
"logo": "QmdKNUFPsB3qUDzNniq1Z5UNgCd6NkeucAdr2y7H2FmnJF",
|
||||
"name": "Pole Position (USA) (A00ED3D6)",
|
||||
"vid": "QmUSws3tEN8qcFUCcS7vegLDs4gRwsudDcD8PAE94vHkeu"
|
||||
},
|
||||
"87248DCD3DB51C1EBF3F8E07E21EC021EEC0B9BE": {
|
||||
"logo": "QmRVhA3yZJbSdkD1evpnpKHutDxdwAtEpWGJwF4fDCV3JQ",
|
||||
"name": "Polar Rescue (USA)",
|
||||
"vid": "QmNmQyQSUy4kf2nbsqbQ9zTWw5VLSNCXDK3GnL8WhvNJVj"
|
||||
},
|
||||
"8BBA6705E6F69F55AB6D105EAC2FD5B0C0D16D9F": {
|
||||
"logo": "QmNhbeQnhDomVvUjN4g98dNQgnpGfnHjdxhnZH2vy11WtB",
|
||||
"name": "Spike (USA, Europe)",
|
||||
"vid": "QmNP7rHCqhNAPNYAhYoSke21dEGJdmY4HdX6CgD1wX2ScZ"
|
||||
},
|
||||
"8F35BAC5E8E0FC4AD7291BBFE5AE6E693272DE06": {
|
||||
"name": "Art Master (USA)"
|
||||
},
|
||||
"9681DC5D38A31CB0D0C6C3BD0FC76BFDDE531CCE": {
|
||||
"logo": "QmQmenQKUtDTuzGcrXCFWQ5mddcJGnXfCdk2g2KvSikT3A",
|
||||
"name": "WebWarp (Europe)",
|
||||
"vid": "QmZ6wtHLWez5EWXpK9DLKW4Bbwn2MHAJqmzNneb93S97kn"
|
||||
},
|
||||
"969D5EC7D7ABA9AC25E54069149D9586217AB63D": {
|
||||
"name": "Mail Plane (USA) (Proto) (05838962)"
|
||||
},
|
||||
"9E5D23CB7EF111394E1804E105B6DD277C062DF6": {
|
||||
"name": "3D Narrow Escape (USA)"
|
||||
},
|
||||
"9F5B0B5E6CAF6D887D3637ACE70AD6A6258B999D": {
|
||||
"name": "Pole Position (USA) (C10F37D8)",
|
||||
"ref": "8454D472435E7A6A4CA5FE129162480CA6E84B87"
|
||||
},
|
||||
"A246F1D94A5B062B60784787A1D6D499B21C10CD": {
|
||||
"logo": "QmXfyfqTdJMRN5j5heiiqkiicQFyppQFPpPhNR7mEZcxLn",
|
||||
"name": "HyperChase - Auto Race (World)",
|
||||
"vid": "Qmafws3VndL7PSxxzZLx6KpLL2Nf8NxZU2NZZPDgNqnxBj"
|
||||
},
|
||||
"AA1AA811C9CE5E19F43B78A6531CDD0E707337BD": {
|
||||
"name": "Blitz! - Action Football (USA, Europe) (881B27C5)",
|
||||
"ref": "60893DFADA42CF50C94083D45AD5BD14D0C031C8"
|
||||
},
|
||||
"B7BD8C8003C60535E1E776758F5C289AA9A77E81": {
|
||||
"name": "Polar Rescue (USA) (Beta)",
|
||||
"ref": "87248DCD3DB51C1EBF3F8E07E21EC021EEC0B9BE"
|
||||
},
|
||||
"C226D78301DBE40F2194C165F2124D5AFA885BE4": {
|
||||
"name": "Engine Analyzer (USA) (Proto)"
|
||||
},
|
||||
"C9B79F1BA30DCBCE5A068B606A97775CF863FE0A": {
|
||||
"logo": "QmZTwQkihBa2VeUgHKGVAJnQ2o1x8cL7sLqtWR8AizhWBL",
|
||||
"name": "Star Castle (USA)",
|
||||
"vid": "QmUFK8JWdwMnToSanY6mKcPPCURXepY71ug4dkSgeWFkip"
|
||||
},
|
||||
"D182EAC5A54FFC642FCBE4A495C7C09F87B43585": {
|
||||
"logo": "QmTnYiP8VBd3tXtJKhRpM1d3fmeeUBDTB1H3NTmav7hwNN",
|
||||
"name": "Star Ship (Europe)",
|
||||
"vid": "QmfA9z3dvG6HRtyzKgChGSLvqVFMGFGSzREwbKE9JuDim2"
|
||||
},
|
||||
"D6B388B3951E34971C1B00C7FDF8DD00AD52DF09": {
|
||||
"logo": "QmZYtvLf6opgdsBMUUic88kGmdjz1iSE5prHwQvjDVYEa5",
|
||||
"name": "StarHawk (World)",
|
||||
"vid": "QmTkv8Lsq78Ht3TKP89Jjor3nR4MhT245MMDtxa3jGsBmb"
|
||||
},
|
||||
"DBB0B378AACC2A18DFB476AA99AB00D6AB395205": {
|
||||
"logo": "QmdQZhuLH5nzGTSX9rw6Co6dsTMySmurQ2kWXSesmCvfYw",
|
||||
"name": "Dark Tower (USA) (Proto)",
|
||||
"vid": "QmdgHceUf86DcvZYmZTWhMZVj6WMWELC8ugFB6DgE3U4T5"
|
||||
},
|
||||
"E3418055F1E79A2300572C557E71BF2CE8BADBFA": {
|
||||
"name": "Test Cartridge (USA) (Rev 4) (Proto)"
|
||||
},
|
||||
"E4BADD494ED548721328EF869D33AD70E3E522C9": {
|
||||
"logo": "Qmb3D82JxKqf7KFHXGSbTR5cBojXbZYUjwRqhpV8NPUPwn",
|
||||
"name": "Berzerk (World)",
|
||||
"vid": "QmTBGNBX64jyNVhW3QmJyXWzrXMayNunhg7cZ5ixYCi5Ew"
|
||||
},
|
||||
"EB76F860130F8CC87826FA931350467760511213": {
|
||||
"logo": "QmTaFSMSwyHEmbsJiQz5Fopa3pJJJFoGViVARw5fhDH3db",
|
||||
"name": "Armor..Attack (World)",
|
||||
"vid": "QmNjJ6K9gL5U3N4atFXs3FBeuaZNJKXuPMcNkU2ALVfcC8"
|
||||
},
|
||||
"F6F26BEB20BB364761A9466FF4087C7B9713D048": {
|
||||
"name": "Pitcher's Duel (USA) (Proto)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2261
metadata/ws.json
2261
metadata/ws.json
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "emulatorjs",
|
||||
"version": "1.3.2",
|
||||
"version": "1.9.2",
|
||||
"description": "Rom and art management utility for generating configuration files for use with the EmulatorJS frontend",
|
||||
"main": "index.js",
|
||||
"author": "thelamer",
|
||||
"dependencies": {
|
||||
"cloudcmd": "^15.9.8",
|
||||
"express": "^4.17.1",
|
||||
"ipfs-http-client": "^53.0.1",
|
||||
"socket.io": "^4.3.1"
|
||||
"cloudcmd": "^15.9.14",
|
||||
"deepmerge": "^4.2.2",
|
||||
"express": "^4.17.2",
|
||||
"ipfs-http-client": "51.0.0",
|
||||
"jszip": "^3.7.1",
|
||||
"socket.io": "^4.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
145
profile.js
Normal file
145
profile.js
Normal file
@ -0,0 +1,145 @@
|
||||
// NPM modules
|
||||
var crypto = require('crypto');
|
||||
var home = require('os').homedir();
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var fs = require('fs');
|
||||
var fsw = require('fs').promises;
|
||||
var path = require('path');
|
||||
var JSZip = require('jszip');
|
||||
|
||||
// Default vars
|
||||
var error = {status: 'error'};
|
||||
app.use(express.json({ limit: '500MB' }));
|
||||
|
||||
// Catch all to detect endpoint
|
||||
app.get('/*', function(req, res) {
|
||||
res.send('pong');
|
||||
});
|
||||
|
||||
// Catch all for any post
|
||||
app.post('/*', async function(req, res) {
|
||||
try {
|
||||
let type = req.body.type;
|
||||
// Send default profile unauthenticated
|
||||
if (type == 'default') {
|
||||
try {
|
||||
let profilePath = home + '/profile/default/';
|
||||
let zip = new JSZip();
|
||||
let items = await fs.readdirSync(profilePath);
|
||||
async function addToZip(item) {
|
||||
if (fs.lstatSync(item).isDirectory()) {
|
||||
let items = await fs.readdirSync(item);
|
||||
if (items.length > 0) {
|
||||
for await (let subPath of items) {
|
||||
await addToZip(item + '/' + subPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let data = fs.readFileSync(item);
|
||||
let zipPath = item.replace(profilePath,'');
|
||||
zip.file(zipPath, data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for await (let item of items) {
|
||||
await addToZip(profilePath + item);
|
||||
}
|
||||
zip.generateAsync({type:"base64"}).then(function callback(base64) {
|
||||
res.json({status: 'success',data: base64});
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
res.json(error);
|
||||
}
|
||||
} else {
|
||||
let auth = req.body.user + req.body.pass;
|
||||
// Simple hash auth
|
||||
let hash = crypto.createHash('sha256').update(auth).digest('hex');
|
||||
let profileJson = await fsw.readFile(home + '/profile/profile.json', 'utf8');
|
||||
let profile = JSON.parse(profileJson);
|
||||
if (profile.hasOwnProperty(hash)) {
|
||||
// Return username if found
|
||||
if (type == 'login') {
|
||||
res.json({status: 'success', user: profile[hash].username});
|
||||
// Take client data and write it to profile
|
||||
} else if (type == 'push') {
|
||||
let profilePath = home + '/profile/' + profile[hash].username + '/';
|
||||
let baseData = req.body.data;
|
||||
// Purge current storage
|
||||
let items = await fsw.readdir(profilePath);
|
||||
if (items.length > 0) {
|
||||
for await (let item of items) {
|
||||
var filePath = profilePath + item;
|
||||
if (fs.statSync(filePath).isFile()) {
|
||||
await fsw.rm(filePath);
|
||||
} else {
|
||||
await fsw.rm(filePath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load zip from data
|
||||
let zip = new JSZip();
|
||||
zip.loadAsync(baseData, {base64: true}).then(async function(contents) {
|
||||
// Unzip the files to the FS by name
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (fileName.endsWith('/')) {
|
||||
if (! fs.existsSync(profilePath + fileName)) {
|
||||
await fsw.mkdir(profilePath + fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
for await (let fileName of Object.keys(contents.files)) {
|
||||
if (! fileName.endsWith('/')) {
|
||||
zip.file(fileName).async('arraybuffer').then(async function(content) {
|
||||
await fsw.writeFile(profilePath + fileName, Buffer.from(content));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
res.json({status: 'success',user: profile[hash].username});
|
||||
// Send client data to write to indexedDB
|
||||
} else if (type == 'pull') {
|
||||
try {
|
||||
let profilePath = home + '/profile/' + profile[hash].username + '/';
|
||||
let zip = new JSZip();
|
||||
let items = await fs.readdirSync(profilePath);
|
||||
async function addToZip(item) {
|
||||
if (fs.lstatSync(item).isDirectory()) {
|
||||
let items = await fs.readdirSync(item);
|
||||
if (items.length > 0) {
|
||||
for await (let subPath of items) {
|
||||
await addToZip(item + '/' + subPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let data = fs.readFileSync(item);
|
||||
let zipPath = item.replace(profilePath,'');
|
||||
zip.file(zipPath, data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for await (let item of items) {
|
||||
await addToZip(profilePath + item);
|
||||
}
|
||||
zip.generateAsync({type:"base64"}).then(function callback(base64) {
|
||||
res.json({status: 'success',data: base64});
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
res.json(error);
|
||||
}
|
||||
} else {
|
||||
res.json(error);
|
||||
}
|
||||
} else {
|
||||
res.json(error);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3001);
|
||||
@ -51,21 +51,14 @@
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
#modal p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
max-width: 800px !important;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Roms page 2 column layout */
|
||||
#header {
|
||||
grid-area: header;
|
||||
h3 {
|
||||
margin-top: .25em;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
#right {
|
||||
@ -78,9 +71,8 @@
|
||||
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-columns: 49.5% 49.5%;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"identified unidentified";
|
||||
}
|
||||
|
||||
@ -94,6 +86,12 @@
|
||||
|
||||
.itemwrapper {
|
||||
display: table;
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.itemwrapper + .itemwrapper {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
@ -101,10 +99,9 @@
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.rombutton {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
.itemwrapper:hover {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.identifybutton {
|
||||
@ -262,3 +259,83 @@
|
||||
height: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
#close {
|
||||
position: absolute;
|
||||
background: black;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#modal {
|
||||
display: none;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
width: 70vw;
|
||||
height: 90vh;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
background: white;
|
||||
z-index: 2;
|
||||
border: 1px solid black;
|
||||
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
#modal-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#preview-iframe {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#rom-manage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#rom-manage p {
|
||||
margin: 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#manage-buttons {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height 30%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.manage-button {
|
||||
border: 2px solid #000;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 3%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.manage-button:hover {
|
||||
background-color: #D3D3D3;
|
||||
}
|
||||
|
||||
#vidPos {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@ -3,15 +3,15 @@
|
||||
<head>
|
||||
<title>EmulatorJS Manager</title>
|
||||
<link rel="stylesheet" href="public/css/index.css">
|
||||
<link rel="stylesheet" href="public/css/vendor/jquery.modal.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="modal" class="modal"></div>
|
||||
<div id="top" class="top">
|
||||
<div id="nav" class="nav">
|
||||
<div onclick="renderRoms()" class="top-item">Rom Management</div>
|
||||
<div onclick="renderConfigss()" class="top-item">Config Management</div>
|
||||
<div onclick="renderFiles()" class="top-item">File Management</div>
|
||||
<div onclick="renderProfile()" class="top-item">Profile Management</div>
|
||||
<div onclick="renderMeta()" class="top-item">Custom Metadata</div>
|
||||
</div>
|
||||
<div id="nav-buttons" class="nav-buttons"></div>
|
||||
</div>
|
||||
@ -19,21 +19,41 @@
|
||||
<div id="side"></div>
|
||||
<div id="main"></div>
|
||||
</div>
|
||||
<div id="modal">
|
||||
<div id="modal-content"></div>
|
||||
<div id="close" onclick="closeModal()"></div>
|
||||
</div>
|
||||
<div id="rominfo" class="hidden">
|
||||
<div class="readme">
|
||||
<h2>Support</h2>
|
||||
<p>For general support please use our <a href="https://discord.gg/YWrKVTn">Discord Server</a>, if you would like to report a bug or have a feature request please do that on <a href="https://github.com/linuxserver/emulatorjs/issues">GitHub</a>.</p>
|
||||
<p>If you are upgrading from previous versions and not seeing a performance increase please do a hard reload on your browser and delete the retroarch.cfg file from the file browser icon (top left of frontent interface folder icon).</p>
|
||||
<p>If you are upgrading from previous versions the n64 core has been switched to a libretro core you will need to change the config file to have '"emulator": "libretro-mupen64plus_next"' under the defaults section of the n64 config to use the new core.</p>
|
||||
<h2>Basic controls</h2>
|
||||
<p>For all libretro based emulators you can use F1 on your keyboard or start+select+L+R on a gamepad to bring up the in game menu.</p>
|
||||
<p>For all libretro based emulators you can use F1 on your keyboard, start+select+L+R on a gamepad , or the touch button on the top left in touchscreen devices to bring up the in game menu.</p>
|
||||
<p>All saves and any settings are stored in your actual web browser by the URL you are using to access this container, to ensure sram is dumped on exit always try to exit the game with back instead of closing the browser window. On libretro based emulators you can perform saves and savestates from the menu.</p>
|
||||
<p>Xbox users on Edge hold the start button for 3 seconds to exit gamepad controls and if you want to re-enter again press the start button again with your cursor hovering over the game and select the option to use gamepad controls. After you launch a game press the select button a couple times to ensure the B button does not trigger a back action or drop you from fullscreen.</p>
|
||||
<p>Scrolling large collections can be sped up by holding right while scrolling up or down when using a keyboard or your controller on Xbox. For touch devices panning is supported by holding and dragging up or down.</p>
|
||||
<p>You can also scroll by letter using the right and left shoulder buttons on a controller or Page Up/Page Down on a keyboard.</p>
|
||||
<h2>Browser support</h2>
|
||||
<p>Only Safari for Apple devices and Chromium based browsers are supported. Other browsers might work, but have not been tested and likely never will be. Gamepads do not function on Firefox, this is a known issue as they do very non standard mapping like dpad button presses to a fake anaolgue joypad.</p>
|
||||
<h2>If you enjoy this</h2>
|
||||
<p>Consider <a href="https://retroarch.com/index.php?page=donate">supporting</a> libretro they do the heavy lifting. Also take a look at helping with art assets <a href="https://github.com/linuxserver/emulatorjs">Here</a>.</p>
|
||||
<h2>Experimental Emulators</h2>
|
||||
<p>More emulators are included than listed below please see <a href="https://github.com/linuxserver/libretrojs">HERE</a> for a complete list of libretro cores. To use an alternative/experimental core just add "libretro-" to the beginning of it's name for the emulator setting for your system config.</p>
|
||||
<h3>Full speed Nintendo DS</h3>
|
||||
<p>Threaded Nintendo DS emulation is available but requires you serve the content from an HTTPS endpoint which means you will need to put this application behind a reverse proxy, we highly recommend <a href="https://github.com/linuxserver/docker-swag">SWAG</a> to achieve this. DS game compatibility and functionality is greatly expanded by using this threaded emulator particularly on an Xbox console the games run at full speed.</p>
|
||||
<p>To enable this emulator just change <span style="background-color:silver;">"emulator": "libretro-melonds"</span> to <span style="background-color:silver;">"emulator": "libretro-melonds_threaded"</span> in the config file.</p>
|
||||
<h3>Full speed Sega Saturn</h3>
|
||||
<p>Threaded Saturn is available but requires you serve the content from an HTTPS endpoint which means you will need to put this application behind a reverse proxy, we highly recommend <a href="https://github.com/linuxserver/docker-swag">SWAG</a> to achieve this. The non threaded emulator is essentially useless unless running some 2D games, threaded many titles run full speed especially on Xbox, if you find any game lacking try setting the threads to 8 in the RetorArch options menu.</p>
|
||||
<p>To enable this emulator just change <span style="background-color:silver;">"emulator": "libretro-yabause"</span> to <span style="background-color:silver;">"emulator": "libretro-yabause_threaded"</span> in the config file.</p>
|
||||
<h2>File Downloading</h2>
|
||||
<p>As the network of peers grows, art and default file downloading might not be a 100% perfect. You can always click download multiple times, items that have allready been downloaded will pull from your local IPFS node running in this container while missing items will be retried. Please try to download a couple times before reaching out for support. If you are able to we would appreciate if you can forward port 4001 from this container to the public internet, it decentralizes the distribution of files leading to a more robust CDN for all users.</p>
|
||||
<h2>Rom Identification basics</h2>
|
||||
<p>The basic steps for scanning in roms is:</p>
|
||||
<ol>
|
||||
<li>Place the roms in the "roms" directory for your system in your mounted /data/ directory.</li>
|
||||
<li>Click one the corresponding scan button above.</li>
|
||||
<li>Click on the corresponding scan button above.</li>
|
||||
<li>Open the item from the menu on the left.</li>
|
||||
<li>If any of your roms are unidentified take the time to link them to the correct rom name.</li>
|
||||
<li>Click to download all the art items for your identified roms.(logos, videos, etc)</li>
|
||||
@ -41,8 +61,29 @@
|
||||
</ol>
|
||||
<p>From there you can hop into the frontend interface (default port 80) to browse and play your collection.</p>
|
||||
<p>In general this application is referencing metadata based on commonly used identifying metrics for rom files, mostly these revolve around checksums (specifically sha1) of the rom files themselves. This metadata is compiled from lists on no-intro.org for roms and redump.org for CD based games. Now no system is perfect, but the aim is to have community participation expand these lists with the data from their own rom sets. After scanning in your roms you may notice that many of them are un-identified using this metadata, if you took the time to link them to the correct reference information please consider providing your resulting reference data to Github <a href="https://github.com/linuxserver/emulatorjs">Here</a>.</p>
|
||||
<h2>Multi Disk PSX</h2>
|
||||
<p>The only web emulator that supports multiple CD games is psx, in order to function please rename your chd files before scanning them in to the same name with the extension .disk1,.disk2,.disk3,etc. IE:</p>
|
||||
<h2>Rom Hacks Support</h2>
|
||||
<p>Retroarch supports <a href="https://docs.libretro.com/guides/softpatching/">Softpatching</a>, which will apply a patchfile to the base rom on launch. In order to leverage this feature you will need to prepare a custom zip file ending with the file extension ".patchzip". Lets take a popular hack "Invictus" as an example for snes. You will need to create a zip file named "Invictus.smc.patchzip" (note the two file extensions with smc indicating the extension of the base rom) containing the following files:</p>
|
||||
<ul>
|
||||
<li>Invictus.smc - base rom file to be patched in this case SMW.</li>
|
||||
<li>Invictus.bps - Patch file to be applied.</li>
|
||||
</ul>
|
||||
<p>Multiple patch files can be applied as long as they follow this naming scheme ending in the order to be loaded IE:</p>
|
||||
<ul>
|
||||
<li>Invictus.smc - base rom file to be patched in this case SMW.</li>
|
||||
<li>Invictus.bps - First patch file to be applied.</li>
|
||||
<li>Invictus.bps1 - Second patch file to be applied.</li>
|
||||
</ul>
|
||||
<p>This patchzip format can also be used for any multifile rom like SNES MSU1 Enhanced games that are supported by Snes9X, for example A Link to the Past MSU1 create a folder containing: (this is just an example some of these files might not be required)</p>
|
||||
<ul>
|
||||
<li>alttp_msu.sfc - base rom file that has been patched to support enhanced audio.</li>
|
||||
<li>manifest.bml - MSU1 manifest with tracklist and memory addresses.</li>
|
||||
<li>alttp_msu.xml - XML manifest.</li>
|
||||
<li>alttp_msu.msu - Video data.</li>
|
||||
<li>alttp_msu-[1-xx].pcm - Audio data files.</li>
|
||||
</ul>
|
||||
<p>Just like the rom hacks name the zip file the same as the rom name in this example "alttp_msu.sfc.patchzip"</p>
|
||||
<h2>Multi Disk Games</h2>
|
||||
<p>The only web emulators that supports multiple CD games are psx and saturn, in order to function please rename your chd files before scanning them in to the same name with the extension .disk1,.disk2,.disk3,etc. IE:</p>
|
||||
<ul>
|
||||
<li>/data/psx/roms/GAMENAME.disk1</li>
|
||||
<li>/data/psx/roms/GAMENAME.disk2</li>
|
||||
@ -50,6 +91,8 @@
|
||||
</ul>
|
||||
<h2>MAME roms with chds</h2>
|
||||
<p>The web based arcade emulator is MAME 2003-Plus, in general most games will work if you use full non-merged roms. Some games require additional chd files though. In order to get these to load with the web frontend we have a custom file format that can be loaded by the extension ".multizip". To create this file simply zip the rom zip and the chd file (no folders) into a single file. IE if you had the file kinst.zip and the folder kinst/kinst.chd simply move the kinst.zip file into the kinst folder and create the archive using <b>zip kinst.multizip kinst.zip kinst.chd</b> then move the resulting file to your roms directory. This multizip file will be unpacked before the emulator is loaded into the directory it is looking for the files.</p>
|
||||
<h2>Doom wads with multiple wads</h2>
|
||||
<p>The web based emulator is prboom, Zdoom pk3 style mods are not supported. Most community wads out there will also require stock doom2 or doomu wads to function. In order to get these to load with the web frontend we have a custom file format that can be loaded by the extension ".multiwad". To create this file simply zip the multiple wads into a single file. IE if you had the file Valiant.wad needing doom2.wad simply move them into the same directory and run <b>zip Valiant.multiwad Valiant.wad doom2.wad</b> then move the resulting file to your roms directory. This multiwad file will be unpacked before the emulator is loaded and all files made available.</p>
|
||||
<h2>Expanding on identification</h2>
|
||||
<p>For all CD based system only bin/cue and chd files are supported for identification purposes, you may be able to use iso files, but automatic identification will not be possible. The only system without chd support is 3do, in all other cases you are best off using chd based roms.<p>
|
||||
<p>Depending on the type of game you are trying to scan in this process can take some time, particularly for CD based games in CHD format as we need to pull out individual tracks in many cases to sha1sum the resulting data. In general identifying CD based games is very difficult and in some cases these will simply be lists of file names as there is not a reliable way to compare the sums of different tracks from the CDs. We use the following methods to try to create unique metadata strings to look up for each rom. If a rom is zipped or 7zipped it will be extracted and the resulting file checksummed.</p>
|
||||
@ -88,7 +131,7 @@
|
||||
<h2>General Emulation</h2>
|
||||
<p>For the most part all non CD based systems will work fine on mobile devices and the Xbox web browser. In general CD emulation for full sized non stripped games is not going to function outside of a decent PC setup. Mobile and game console browsers lack the ability to dedicate the 100s of megs of memory needed to store and run a CD based game. Sound crackling is normal in web based emulation, in many cases it will actually improve as the game is played longer. Please keep in mind what this is, these are full emulators re-compiled into javascript do not expect perfection. Emulators used with this project:</p>
|
||||
<ul>
|
||||
<li>3DO- <a href="https://www.emulatorjs.com/3do.html">https://www.emulatorjs.com/3do.html</a></li>
|
||||
<li>3DO- <a href="https://emulatorjs.org/github/docs/Systems/3DO.html">https://emulatorjs.org/github/docs/Systems/3DO.html</a></li>
|
||||
<li>Arcade- <a href="https://github.com/libretro/mame2003-plus-libretro">https://github.com/libretro/mame2003-plus-libretro</a></li>
|
||||
<li>Atari 2600- <a href="https://github.com/libretro/stella2014-libretro">https://github.com/libretro/stella2014-libretro</a></li>
|
||||
<li>Atari 7800- <a href="https://github.com/libretro/prosystem-libretro">https://github.com/libretro/prosystem-libretro</a></li>
|
||||
@ -100,14 +143,14 @@
|
||||
<li>Jaguar- <a href="https://github.com/libretro/virtualjaguar-libretro">https://github.com/libretro/virtualjaguar-libretro</a></li>
|
||||
<li>Atari LYNX- <a href="https://github.com/libretro/libretro-handy">https://github.com/libretro/libretro-handy</a></li>
|
||||
<li>MSX- <a href="https://github.com/libretro/blueMSX-libretro">https://github.com/libretro/blueMSX-libretro</a></li>
|
||||
<li>Nintendo 64- <a href="https://www.emulatorjs.com/n64.html">https://www.emulatorjs.com/n64.html</a></li>
|
||||
<li>Nintendo 64- <a href="https://github.com/libretro/mupen64plus-libretro-nx">https://github.com/libretro/mupen64plus-libretro-nx</a></li>
|
||||
<li>Nintendo DS- <a href="https://github.com/libretro/melonDS">https://github.com/libretro/melonDS</a></li>
|
||||
<li>Nintendo Entertainment System- <a href="https://github.com/libretro/libretro-fceumm">https://github.com/libretro/libretro-fceumm</a></li>
|
||||
<li>Neo Geo Pocket (color)- <a href="https://github.com/libretro/beetle-ngp-libretro">https://github.com/libretro/beetle-ngp-libretro</a></li>
|
||||
<li>Odyssey2- <a href="https://github.com/libretro/libretro-o2em">https://github.com/libretro/libretro-o2em</a></li>
|
||||
<li>PC Engine (TurboGrafx-16)- <a href="https://github.com/libretro/beetle-pce-fast-libretro">https://github.com/libretro/beetle-pce-fast-libretro</a></li>
|
||||
<li>Playstation- <a href="https://github.com/thelamer/beetle-psx-libretro">https://github.com/thelamer/beetle-psx-libretro</a></li>
|
||||
<li>Sega 32x- <a href="https://www.emulatorjs.com/sega32x.html">https://www.emulatorjs.com/sega32x.html</a></li>
|
||||
<li>Playstation- <a href="https://github.com/thelamer/retrostash/tree/master/beetle-psx-libretro">https://github.com/thelamer/retrostash/tree/master/beetle-psx-libretro</a></li>
|
||||
<li>Sega 32x- 32x currently has no functional emulator I can distribute.</a></li>
|
||||
<li>Sega CD- <a href="https://github.com/libretro/Genesis-Plus-GX">https://github.com/libretro/Genesis-Plus-GX</a></li>
|
||||
<li>Sega Game Gear- <a href="https://github.com/libretro/Genesis-Plus-GX">https://github.com/libretro/Genesis-Plus-GX</a></li>
|
||||
<li>Sega Mega Drive- <a href="https://github.com/libretro/Genesis-Plus-GX">https://github.com/libretro/Genesis-Plus-GX</a></li>
|
||||
@ -119,8 +162,6 @@
|
||||
<li>Virtual Boy- <a href="https://github.com/libretro/beetle-vb-libretro">https://github.com/libretro/beetle-vb-libretro</a></li>
|
||||
<li>Wonderswan (color)- <a href="https://github.com/libretro/beetle-wswan-libretro">https://github.com/libretro/beetle-wswan-libretro</a></li>
|
||||
</ul>
|
||||
<h2>Experimental Emulators</h2>
|
||||
<p>More emulators are included than listed above the libretro psx emulator is functional but has more serious memory requirements please see <a href="https://github.com/linuxserver/libretrojs">HERE</a> for a complete list of libretro cores. To enable the libretro psx emulator change your psx config to use "libretro-mednafen_psx" as the emulator and "psx.zip" as the bios. To use an alternative/experimental core just add "libretro-" to the beginning of it's name for the emulator setting for your system config.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="landing" class="hidden">
|
||||
@ -134,9 +175,16 @@
|
||||
<h1>Choose a folder. Drag and drop files or right click to upload.</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="metainfo" class="hidden">
|
||||
<div class="readme">
|
||||
<h2>Custom Metadata</h2>
|
||||
<p>When you identify roms or upload custom artwork from the rom management interface all of that data is stored as custom metadata in your container. You can see the systems you have put the work into either skinning or identifying on the left. On top of that when you upload custom art assets from this web interface those items are added to the local IPFS server running in this Docker container. To share this data with the world you will need to forward port 4001 to a public IP address.</p>
|
||||
<h2>Help us out</h2>
|
||||
<p>You went through the process of linking up roms and cleaning up the artwork the way you like it, odds are another person would appreciate not doing the same work you did. If you are able to please share your metadata to <a href="https://github.com/linuxserver/emulatorjs/issues">GitHub</a> or hop on our <a href="https://discord.gg/YWrKVTn">Discord Server</a> and share it there. We can help with file optimization and getting stuff pinned to IPFS permanently. Just copy and paste the metadata to something like <a href="https://pastebin.com/">Paste Bin</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script src="socket.io/socket.io.js"></script>
|
||||
<script src="public/js/vendor/jquery.min.js"></script>
|
||||
<script src="public/js/vendor/jquery.modal.min.js"></script>
|
||||
<script src="public/js/vendor/ace.js"></script>
|
||||
<script src="public/js/index.js"></script>
|
||||
</body>
|
||||
|
||||
@ -23,6 +23,14 @@ socket.on('modaldata', modalData);
|
||||
socket.on('emptymodal', emptyModal);
|
||||
// Render file directories
|
||||
socket.on('renderfiledirs', renderFileDirs);
|
||||
// Render file directories
|
||||
socket.on('renderprofiles', renderProfiles);
|
||||
// Render in rom data
|
||||
socket.on('romdata', renderRomData);
|
||||
// Render in custom metadata page
|
||||
socket.on('rendermeta', renderMetaPage);
|
||||
// Render meta JSON
|
||||
socket.on('rendermetajson', renderMetaJSON);
|
||||
|
||||
//// Functions ////
|
||||
// Grab a json file from the server
|
||||
@ -31,18 +39,39 @@ function getConfig(file) {
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
$('#main').data('name', file);
|
||||
socket.emit('getconfig', file);
|
||||
};
|
||||
}
|
||||
|
||||
// Grab a json file from the server
|
||||
function getMeta(file) {
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
$('#main').data('name', file);
|
||||
socket.emit('getmeta', file);
|
||||
}
|
||||
|
||||
// Get list of rom shas to process
|
||||
function getRomShas(dir) {
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
$('#main').data('name', dir);
|
||||
socket.emit('getroms', dir);
|
||||
};
|
||||
}
|
||||
|
||||
// Render configs page
|
||||
function renderConfigss() {
|
||||
socket.emit('renderconfigs');
|
||||
};
|
||||
}
|
||||
|
||||
// Render meta page
|
||||
function renderMeta() {
|
||||
socket.emit('rendermeta');
|
||||
}
|
||||
|
||||
// Render profiles page
|
||||
function renderProfile() {
|
||||
socket.emit('renderprofiles');
|
||||
}
|
||||
|
||||
// Render roms landing page
|
||||
async function renderRomsLanding(counts) {
|
||||
$('#main').empty();
|
||||
@ -73,7 +102,7 @@ async function renderRomsLanding(counts) {
|
||||
$(cardContainer).append(scanWarning);
|
||||
};
|
||||
$('#main').append(cardContainer, $('#rominfo').html());
|
||||
};
|
||||
}
|
||||
|
||||
// Render roms page
|
||||
function renderRoms() {
|
||||
@ -82,62 +111,41 @@ function renderRoms() {
|
||||
|
||||
// Scan in a roms directory
|
||||
function scanRoms(folder) {
|
||||
socket.emit('scanroms', folder);
|
||||
$('#modal').modal();
|
||||
};
|
||||
socket.emit('scanroms', [folder, true]);
|
||||
$('#modal').toggle(100);
|
||||
}
|
||||
|
||||
// Prompt user to identify rom
|
||||
async function identifyRom(sha, name) {
|
||||
$('#modal').modal();
|
||||
$('#modal').empty();
|
||||
$('#modal').data('sha', sha.replace("|","'"));
|
||||
$('#modal').append('<p>Identify: ' + name.replace("|","'") + '</p>');
|
||||
var metaData = $('#main').data('metadata');
|
||||
var options = [];
|
||||
for await (sha of Object.keys(metaData)) {
|
||||
if (metaData[sha].hasOwnProperty('name')) {
|
||||
options.push({'sha': sha, 'name': metaData[sha].name});
|
||||
}
|
||||
};
|
||||
var sorted = options.sort(function(a, b){
|
||||
const name1 = a.name.toUpperCase();
|
||||
const name2 = b.name.toUpperCase();
|
||||
let comparison = 0;
|
||||
if (name1 > name2) {
|
||||
comparison = 1;
|
||||
} else if (name1 < name2) {
|
||||
comparison = -1;
|
||||
}
|
||||
return comparison;
|
||||
})
|
||||
var sel = $('<select>').attr('id', 'gameselection');
|
||||
for await (var option of sorted) {
|
||||
sel.append($("<option>").attr('value',option.sha).text(option.name));
|
||||
};
|
||||
$('#modal').append(sel);
|
||||
var setMetaButton = $('<button>').addClass('button hover').attr('onclick', 'setMeta()').text('Link Item');
|
||||
$('#modal').append(setMetaButton);
|
||||
};
|
||||
// Scan in a roms directory
|
||||
function newScan(folder) {
|
||||
socket.emit('scanroms', [folder, false]);
|
||||
$('#modal').toggle(100);
|
||||
}
|
||||
|
||||
// Link metadata for selected rom
|
||||
function setMeta() {
|
||||
var foundGameSha = $('#gameselection').val();
|
||||
var romSha = $('#modal').data('sha');
|
||||
var linkHash = $('#gameselection').val();
|
||||
var hash = $('#modal').data('hash');
|
||||
var dir = $('#main').data('name');
|
||||
closeModal();
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
$.modal.close();
|
||||
socket.emit('usermeta', [romSha, foundGameSha, dir]);
|
||||
};
|
||||
socket.emit('usermeta', [hash, linkHash, dir]);
|
||||
}
|
||||
|
||||
// Send output to modal
|
||||
function modalData(data) {
|
||||
$('#modal').prepend('<p>' + data + '</p>');
|
||||
};
|
||||
$('#modal-content').prepend('<p>' + data + '</p>');
|
||||
}
|
||||
|
||||
// Empty modal
|
||||
function emptyModal() {
|
||||
$('#modal').empty();
|
||||
$('#modal-content').empty();
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
emptyModal();
|
||||
$('#modal').toggle(100)
|
||||
}
|
||||
|
||||
// Render config file list
|
||||
@ -151,7 +159,20 @@ function renderConfigs(files) {
|
||||
$('#side').append(sideLink);
|
||||
});
|
||||
$('#main').append($('<h1>').addClass('readme').text('Open a config to edit (main for landing page)'));
|
||||
};
|
||||
}
|
||||
|
||||
// Render meta file list
|
||||
function renderMetaPage(files) {
|
||||
$('#main').empty();
|
||||
$('#main').append($('#metainfo').html());
|
||||
$('#side').empty();
|
||||
$('#nav-buttons').empty();
|
||||
$.each(files, function(index, file) {
|
||||
var file = file.replace('.json','');
|
||||
var sideLink = $('<div>').addClass('sideitem hover').attr('onclick', "getMeta('" + file + "')").text(file);
|
||||
$('#side').append(sideLink);
|
||||
});
|
||||
}
|
||||
|
||||
// Render Roms file list
|
||||
function renderRomsDir(dirs) {
|
||||
@ -161,7 +182,7 @@ function renderRomsDir(dirs) {
|
||||
var sideLink = $('<div>').addClass('sideitem hover').attr('onclick', "getRomShas('" + dir + "')").text(dir);
|
||||
$('#side').append(sideLink);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Save modified config
|
||||
async function saveConfig() {
|
||||
@ -171,30 +192,37 @@ async function saveConfig() {
|
||||
var config = JSON.parse(json);
|
||||
socket.emit('saveconfig', {'config': config, 'name': name});
|
||||
socket.emit('renderconfigs');
|
||||
};
|
||||
}
|
||||
|
||||
// Save romlist to config file
|
||||
function addToConfig(name) {
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('addtoconfig', name);
|
||||
};
|
||||
}
|
||||
|
||||
// Remove roms with no art from config
|
||||
function purgeNoArt(name) {
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('purgenoart', name);
|
||||
}
|
||||
|
||||
// Download art for all identified roms
|
||||
function downloadArt(name) {
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
$('#modal').modal();
|
||||
$('#modal').toggle(100);
|
||||
socket.emit('downloadart', name);
|
||||
};
|
||||
}
|
||||
|
||||
// Tell server to download the default file set
|
||||
function dlDefaultFiles() {
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
$('#modal').modal();
|
||||
$('#modal').toggle(100);
|
||||
socket.emit('dldefaultfiles');
|
||||
};
|
||||
}
|
||||
|
||||
// Render in a config file
|
||||
async function renderConfig(config) {
|
||||
@ -214,7 +242,23 @@ async function renderConfig(config) {
|
||||
});
|
||||
var json = JSON.stringify(config, null, 2);
|
||||
editor.setValue(json, -1);
|
||||
};
|
||||
}
|
||||
|
||||
// Render in a config file
|
||||
async function renderMetaJSON(meta) {
|
||||
// Main edit window
|
||||
$('#main').empty();
|
||||
$('#main').append($('<div>').attr('id', 'editor'));
|
||||
editor = ace.edit('editor');
|
||||
editor.setTheme('ace/theme/chrome');
|
||||
editor.session.setMode('ace/mode/json');
|
||||
editor.$blockScrolling = Infinity;
|
||||
editor.setOptions({
|
||||
readOnly: false,
|
||||
});
|
||||
var json = JSON.stringify(meta, null, 2);
|
||||
editor.setValue(json, -1);
|
||||
}
|
||||
|
||||
// Render in roms data
|
||||
async function renderRom(data) {
|
||||
@ -223,26 +267,24 @@ async function renderRom(data) {
|
||||
$('#main').data('metadata', data[2]);
|
||||
$('#side').empty();
|
||||
// Render Legend
|
||||
var keys = [['All Art Downloaded', 'green'], ['Art Available for Download', 'red'], ['No Art Available', 'gray']]
|
||||
var header = $('<div>').attr('id', 'header');
|
||||
for await (var key of keys) {
|
||||
var item = $('<div>').css('background-color',key[1]).text(key[0]);
|
||||
header.append(item);
|
||||
}
|
||||
var container = $('<div>').addClass('wrapper');
|
||||
container.append(header);
|
||||
let container = $('<div>').addClass('wrapper');
|
||||
$('#main').append(container);
|
||||
var identified = $('<div>').attr('id','left').append('<p>Identified Roms:</p>');
|
||||
var unidentified = $('<div>').attr('id','right').append('<p>Unidentified Roms:</p>');
|
||||
let identified = $('<div>').attr('id','left').append('<h3>Identified Roms:</h3>');
|
||||
let unidentified = $('<div>').attr('id','right').append('<h3>Unidentified Roms:</h3>');
|
||||
container.append(identified,unidentified);
|
||||
// Process buttons
|
||||
var folderName = $('#main').data('name');
|
||||
var downloadArtButton = $('<button>').addClass('button hover').attr('onclick', 'downloadArt(\'' + folderName + '\');').text('Download All Available Art');
|
||||
let folderName = $('#main').data('name');
|
||||
let downloadArtButton = $('<button>').addClass('button hover').attr('onclick', 'downloadArt(\'' + folderName + '\');').text('Download All Available Art');
|
||||
$('#side').append($('<p>').text('Step 1:'));
|
||||
$('#side').append(downloadArtButton);
|
||||
var configButton = $('<button>').addClass('button hover').attr('onclick', 'addToConfig(\'' + folderName + '\');').text('Add All Roms to Config');
|
||||
let configButton = $('<button>').addClass('button hover').attr('onclick', 'addToConfig(\'' + folderName + '\');').text('Add All Roms to Config');
|
||||
$('#side').append($('<p>').text('Step 2:'));
|
||||
$('#side').append(configButton);
|
||||
$('#side').append($('<p>').text('Optional:'));
|
||||
let noArtButton = $('<button>').addClass('button hover').attr('onclick', 'purgeNoArt(\'' + folderName + '\');').text('Remove Roms with No Art');
|
||||
$('#side').append(noArtButton);
|
||||
let newScanButton = $('<button>').addClass('button hover').attr('onclick', 'newScan(\'' + folderName + '\');').text('Scan for New Items');
|
||||
$('#side').append(newScanButton);
|
||||
// Render items
|
||||
for await (var idItem of Object.keys(data[0])) {
|
||||
if (data[0][idItem].has_art == true) {
|
||||
@ -252,37 +294,183 @@ async function renderRom(data) {
|
||||
} else if (data[0][idItem].has_art == 'none') {
|
||||
var color = 'gray';
|
||||
};
|
||||
var deleteButton = $('<button>').addClass('rombutton item').attr('onclick', 'deleteRom(\'' + idItem.replace("'","|") + '\')').text('delete');
|
||||
var item = $('<div>').addClass('itemwrapper').css('background-color', color).html($('<p>').addClass('item').text(idItem));
|
||||
item.append(deleteButton);
|
||||
var item = $('<div>').addClass('itemwrapper').css('background-color', color).html($('<p>').addClass('item').text(idItem)).attr('onclick', 'romMenu(\'' + idItem.replaceAll("'","|") + '\')');
|
||||
identified.append(item)
|
||||
};
|
||||
for await (var noIdItem of Object.keys(data[1])) {
|
||||
var identifyButton = $('<button>').addClass('identifybutton').attr('onclick', 'identifyRom(\'' + data[1][noIdItem].replace("'","|") + '\',\'' + noIdItem.replace("'","|") + '\');').text('identify');
|
||||
var deleteButton = $('<button>').addClass('rombutton item').attr('onclick', 'deleteRom(\'' + noIdItem.replace("'","|") + '\')').text('delete');
|
||||
var item = $('<div>').addClass('itemwrapper').html($('<p>').addClass('item').text(noIdItem).append(identifyButton));
|
||||
item.append(deleteButton);
|
||||
var item = $('<div>').addClass('itemwrapper').css('background-color', 'gray').html($('<p>').addClass('item').text(noIdItem)).attr('onclick', 'romMenu(\'' + noIdItem.replaceAll("'","|") + '\')');
|
||||
unidentified.append(item)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Render confirm delete rom
|
||||
function deleteRom(name) {
|
||||
$('#modal').modal();
|
||||
$('#modal').empty();
|
||||
var dir = $('#main').data('name');
|
||||
var realName = name.replace("|","'");
|
||||
$('#modal').data('delete', [name, dir]);
|
||||
var message = $('<p>').text('Really delete ' + realName + ' ?');
|
||||
var deletebutton = $('<button>').attr('onclick', 'deleteRomReal()').addClass('button hover').text('delete');
|
||||
$('#modal').append(message, deletebutton);
|
||||
};
|
||||
// Render rom menu modal
|
||||
function romMenu(cleanName) {
|
||||
let dir = $('#main').data('name');
|
||||
name = cleanName.replaceAll("|","'");
|
||||
$('#modal').data('name', name);
|
||||
emptyModal();
|
||||
$('#modal-content').append('<div class="loader"></div>');
|
||||
$('#modal').toggle(100);
|
||||
socket.emit('getromdata', [dir, name]);
|
||||
}
|
||||
|
||||
// Delete rom
|
||||
function deleteRomReal(data) {
|
||||
socket.emit('deleterom', $('#modal').data('delete'));
|
||||
$.modal.close();
|
||||
};
|
||||
// Render rom data we get
|
||||
async function renderRomData(data) {
|
||||
$('#modal').data('hash', data.hash);
|
||||
let metaVars = ['back','corner','logo','vid'];
|
||||
let dir = $('#main').data('name');
|
||||
let basePath = 'frontend/user/';
|
||||
emptyModal();
|
||||
previewFrame = $('<iframe>').attr({src: 'frontend/index.html#preview', id: 'preview-iframe'});
|
||||
$('#modal-content').append(previewFrame);
|
||||
let manage = $('<div>').attr('id', 'rom-manage');
|
||||
let fileLink = $('<a>').attr('href', basePath + dir + '/roms/' + data.file).text(data.file);
|
||||
let fileName = $('<p>').text('Rom File: ').append(fileLink);
|
||||
manage.append(fileName);
|
||||
if (data.metadata.hasOwnProperty('name')) {
|
||||
let name = $('<p>').text('Meta Name: ' + data.metadata.name);
|
||||
manage.append(name);
|
||||
} else {
|
||||
let name = $('<p>').text('Meta Name: Unidentified or NA');
|
||||
manage.append(name);
|
||||
}
|
||||
let hash = $('<p>').text('Scanned Hash: ' + data.hash);
|
||||
manage.append(hash);
|
||||
for await (let asset of metaVars) {
|
||||
if (data[asset]) {
|
||||
let link = $('<a>').attr('href', basePath + data[asset]).text(data[asset]);
|
||||
let text = $('<p>').text(asset + ': ').append(link);
|
||||
manage.append(text);
|
||||
} else {
|
||||
let text = $('<p>').text(asset + ': Default or not found');
|
||||
manage.append(text);
|
||||
}
|
||||
}
|
||||
if (data.metadata.hasOwnProperty('video_position')) {
|
||||
var vidPos = data.metadata.video_position;
|
||||
} else {
|
||||
var vidPos = '';
|
||||
}
|
||||
if (data.vid) {
|
||||
let vidInput = $('<p>').text('Video Position: ');
|
||||
let posInput = $('<input>').attr({id: 'vidPos', type: 'text', value: vidPos});
|
||||
let posButton = $('<button>').addClass('button hover').text('Update').attr('onclick', 'updateVidPos()');
|
||||
vidInput.append(posInput,posButton);
|
||||
manage.append(vidInput);
|
||||
}
|
||||
let buttonWrapper = $('<div>').attr('id', 'manage-buttons');
|
||||
if (data.metadata) {
|
||||
for await (let upload of metaVars) {
|
||||
let uploadButton = $('<button>').addClass('manage-button').text('Upload ' + upload);
|
||||
uploadButton.attr('onclick',"$('#" + upload + "').trigger('click')");
|
||||
let uploadInput = $('<input>').addClass('hidden').attr({id: upload, type: 'file', onchange: 'upload(this)'});
|
||||
buttonWrapper.append(uploadButton,uploadInput);
|
||||
}
|
||||
let unIdentifyButton = $('<button>').addClass('manage-button').text('Purge Art/Metadata');
|
||||
buttonWrapper.append(unIdentifyButton.attr('onclick','unIdentify(false)'));
|
||||
} else {
|
||||
let identifyButton = $('<button>').addClass('manage-button').text('Identify Rom');
|
||||
buttonWrapper.append(identifyButton.attr('onclick','identify()'));
|
||||
}
|
||||
let deleteButton = $('<button>').addClass('manage-button').text('Delete Everything');
|
||||
buttonWrapper.append(deleteButton.attr('onclick','unIdentify(true)'));
|
||||
manage.append(buttonWrapper);
|
||||
$('#modal-content').append(manage);
|
||||
}
|
||||
|
||||
// Prompt user to identify rom
|
||||
async function identify() {
|
||||
let file = $('#modal').data('name');
|
||||
$('#rom-manage').empty();
|
||||
$('#rom-manage').append('<p>Identify: ' + file + '</p>');
|
||||
var metaData = $('#main').data('metadata');
|
||||
var options = [];
|
||||
for await (sha of Object.keys(metaData)) {
|
||||
if (metaData[sha].hasOwnProperty('name')) {
|
||||
options.push({'sha': sha, 'name': metaData[sha].name});
|
||||
}
|
||||
};
|
||||
|
||||
var sel = $('<select>').attr('id', 'gameselection');
|
||||
|
||||
// Bump an exact match on the filename to the top of the list
|
||||
var mostLikely = options.filter(opt => file.startsWith(opt.name))
|
||||
for await (var option of mostLikely) {
|
||||
sel.append($("<option>").attr('value',option.sha).text(option.name));
|
||||
}
|
||||
|
||||
var sorted = options.sort((a,b) => (a.name > b.name) ? 1 : -1)
|
||||
for await (var option of sorted) {
|
||||
sel.append($("<option>").attr('value',option.sha).text(option.name));
|
||||
};
|
||||
$('#rom-manage').append(sel);
|
||||
var setMetaButton = $('<button>').addClass('button hover').attr('onclick', 'setMeta()').text('Link Item');
|
||||
$('#rom-manage').append(setMetaButton);
|
||||
$('#rom-manage').append($('<h3>').text('Rom not found?:'));
|
||||
let idInput = $('<p>').text('Custom Rom: ');
|
||||
let nameInput = $('<input>').attr({id: 'customName', type: 'text', placeholder: 'Enter a custom name'});
|
||||
let idButton = $('<button>').addClass('button hover').text('Identify').attr('onclick', 'setCustomMeta()');
|
||||
idInput.append(nameInput,idButton);
|
||||
$('#rom-manage').append(idInput);
|
||||
}
|
||||
|
||||
// Remove identified roms link if it exists
|
||||
function unIdentify(purge) {
|
||||
let hash = $('#modal').data('hash');
|
||||
let dir = $('#main').data('name');
|
||||
let file = $('#modal').data('name');
|
||||
closeModal();
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('removemeta', [hash, dir, file, purge]);
|
||||
}
|
||||
|
||||
// Set custom metadata for a rom
|
||||
function setCustomMeta() {
|
||||
let hash = $('#modal').data('hash');
|
||||
let dir = $('#main').data('name');
|
||||
let name = $('#customName').val();
|
||||
closeModal();
|
||||
$('#main').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('custommeta', [hash, dir, name]);
|
||||
}
|
||||
|
||||
// Handle art uploads
|
||||
async function upload(input) {
|
||||
let fileType = $(input).attr('id');
|
||||
let dir = $('#main').data('name');
|
||||
let file = $('#modal').data('name');
|
||||
let hash = $('#modal').data('hash');
|
||||
if (input.files && input.files[0]) {
|
||||
emptyModal();
|
||||
$('#modal-content').append('<div class="loader"></div>');
|
||||
let reader = new FileReader();
|
||||
reader.onload = async function(e) {
|
||||
if (e.total < 100000000) {
|
||||
let data = e.target.result;
|
||||
socket.emit('uploadart', [fileType, dir, file, hash, data]);
|
||||
} else {
|
||||
emptyModal();
|
||||
$('#modal-content').append($('<h2>').text('File too big'));
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
socket.emit('getromdata', [dir, name]);
|
||||
}
|
||||
}
|
||||
reader.readAsArrayBuffer(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle request to change video position
|
||||
function updateVidPos() {
|
||||
let dir = $('#main').data('name');
|
||||
let file = $('#modal').data('name');
|
||||
let hash = $('#modal').data('hash');
|
||||
let position = $('#vidPos').val();
|
||||
emptyModal();
|
||||
$('#modal-content').append('<div class="loader"></div>');
|
||||
socket.emit('updatevidposition', [dir, file, hash, position]);
|
||||
}
|
||||
|
||||
// Render in landing
|
||||
function renderLanding() {
|
||||
@ -290,7 +478,7 @@ function renderLanding() {
|
||||
$('#main').empty();
|
||||
$('#side').empty();
|
||||
$('#main').append($('#landing').html());
|
||||
};
|
||||
}
|
||||
|
||||
// Render file management
|
||||
function renderFiles() {
|
||||
@ -298,7 +486,7 @@ function renderFiles() {
|
||||
$('#side').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('renderfiles');
|
||||
};
|
||||
}
|
||||
|
||||
// Render file directories
|
||||
function renderFileDirs(dirs) {
|
||||
@ -310,7 +498,7 @@ function renderFileDirs(dirs) {
|
||||
var sideLink = $('<div>').addClass('sideitem hover').attr('onclick', "renderFileBrowser('" + dir + "')").text(dir);
|
||||
$('#side').append(sideLink);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Tell server to configure a file browser
|
||||
function renderFileBrowser(dir) {
|
||||
@ -321,4 +509,59 @@ function renderFileBrowser(dir) {
|
||||
};
|
||||
var browser = $('<iframe>').attr('src', url + 'files/fs/' + dir).addClass('browser');
|
||||
$('#main').append(browser);
|
||||
};
|
||||
}
|
||||
|
||||
// Render profiles
|
||||
function renderProfiles(profiles) {
|
||||
$('#side').empty();
|
||||
$('#main').empty();
|
||||
$('#nav-buttons').empty();
|
||||
$('#main').append($('<h1>').addClass('readme').text('Choose a Profile or Create one:'));
|
||||
let userForm = $('<span>').addClass('readme');
|
||||
userForm.append($('<input>').attr({id:'user',type:'text',placeholder:'username'}));
|
||||
userForm.append($('<input>').attr({id:'pass',type:'password',placeholder:'password'}));
|
||||
userForm.append($('<button>').addClass('button hover').attr('onclick','createProfile()').text('Create Profile'));
|
||||
$('#main').append(userForm);
|
||||
profiles.push('default');
|
||||
profiles.sort();
|
||||
$.each(profiles, function(index, profile) {
|
||||
var sideLink = $('<div>').addClass('sideitem hover').attr('onclick', "renderUserProfile('" + profile + "')").text(profile);
|
||||
$('#side').append(sideLink);
|
||||
});
|
||||
}
|
||||
|
||||
// Render a profile
|
||||
function renderUserProfile(dir) {
|
||||
$('#main').empty();
|
||||
$('#nav-buttons').empty();
|
||||
var url = window.location.href;
|
||||
if (! url.endsWith('/')) {
|
||||
var url = url + '/';
|
||||
};
|
||||
var browser = $('<iframe>').attr('src', url + 'profile/fs/' + dir).addClass('browser');
|
||||
$('#main').append(browser);
|
||||
// Delete Button
|
||||
if (dir !== 'default') {
|
||||
var deleteButton = $('<button>').addClass('button hover').attr('onclick', 'deleteProfile(\'' + dir + '\')').text('Delete ' + dir);
|
||||
$('#nav-buttons').append(deleteButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a blank profile
|
||||
function createProfile() {
|
||||
let user = $('#user').val();
|
||||
let pass = $('#pass').val();
|
||||
$('#main').empty();
|
||||
$('#side').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('createprofile', [user,pass]);
|
||||
}
|
||||
|
||||
// Delete a profile
|
||||
function deleteProfile(user) {
|
||||
$('#main').empty();
|
||||
$('#side').empty();
|
||||
$('#main').append('<div class="loader"></div>');
|
||||
socket.emit('deleteprofile', user);
|
||||
}
|
||||
|
||||
|
||||
35
setup_dev.sh
35
setup_dev.sh
@ -1,24 +1,5 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Working emulators for retroarch
|
||||
retroarchemus="\
|
||||
fceumm
|
||||
snes9x
|
||||
mednafen_vb
|
||||
gearboy
|
||||
vba_next
|
||||
gambatte
|
||||
mgba
|
||||
genesis_plus_gx
|
||||
handy
|
||||
mame2003_plus
|
||||
mednafen_ngp
|
||||
mednafen_wswan
|
||||
o2em
|
||||
prboom
|
||||
tyrquake
|
||||
vecx
|
||||
bluemsx"
|
||||
IFS=$'\n'
|
||||
rootdir=$(pwd)
|
||||
# Install modules
|
||||
@ -27,24 +8,10 @@ npm install
|
||||
# EmulatorJS
|
||||
curl -o \
|
||||
/tmp/emulatorjs-blob.tar.gz -L \
|
||||
"https://github.com/ethanaobrien/emulatorjs/archive/main.tar.gz"
|
||||
"https://github.com/thelamer/emulatorjs/archive/main.tar.gz"
|
||||
tar xf \
|
||||
/tmp/emulatorjs-blob.tar.gz -C \
|
||||
frontend/ --strip-components=1
|
||||
# Libretro emscripten
|
||||
mkdir retrotmp
|
||||
cd retrotmp
|
||||
wget https://buildbot.libretro.com/nightly/emscripten/RetroArch.7z
|
||||
7z x RetroArch.7z
|
||||
sed -i 's/wasmBinaryFile="/wasmBinaryFile="data\//g' retroarch/*.js
|
||||
for emu in $retroarchemus; do
|
||||
mv retroarch/${emu}_libretro.* "${rootdir}/frontend/data/"
|
||||
done
|
||||
cd retroarch/assets/frontend/bundle/
|
||||
zip -r frontend.zip assets/xmb/monochrome assets/ozone shaders filters info autoconfig overlay assets/menu_widgets
|
||||
mv frontend.zip "${rootdir}/frontend/data/"
|
||||
cd "${rootdir}"
|
||||
rm -Rf retrotmp
|
||||
# Custom cores
|
||||
curl -o \
|
||||
/tmp/custom-cores.tar.gz -L \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user