Compare commits

...

187 Commits

Author SHA1 Message Date
thelamer
4573321563 bump version 2024-09-12 13:40:30 -04:00
thelamer
bd7f6d3f88 add metadata submitted by @gr33k 2024-09-12 13:31:36 -04:00
thelamer
82b3a6a6a0 bump version 2024-06-07 10:21:04 -04:00
thelamer
557f22224e double the ipfs timeout to facilitate larger files 2024-06-07 10:20:18 -04:00
Ryan Kuba
b387fb9cc3
Merge pull request #145 from codingismy11to7/patch-1
Fix administration of files with multiple apostrophes
2024-05-30 17:26:42 -04:00
Steven Scott
b623e88332
Fix administration of files with multiple apostrophes 2024-05-30 16:58:04 -04:00
thelamer
f0ab089cf0 add notes on MSU1 support for snes9x via patchzip 2024-03-30 12:42:41 -07:00
thelamer
6f8a9ce8c2 add arcade samples to default fileset as bios payload, use overlayfs for system mounting, bump version 2024-03-30 12:21:16 -07:00
thelamer
e4bcad3fc0 bump version 2024-03-11 13:44:14 -07:00
thelamer
57064e9845 add analog scroll and isolate up down to own statement for dpad 2024-03-10 14:59:55 -07:00
thelamer
9f9f47d1c8 function location 2024-03-08 11:52:59 -08:00
thelamer
e65c91993d stop blocking out edge and try not to break controller in menu 2024-03-08 11:46:07 -08:00
thelamer
3f36deb276 rescale 2024-02-08 09:54:54 -08:00
thelamer
ea4519329b update pwa icon and make sure it works over authed 2024-02-08 09:51:33 -08:00
thelamer
7cdb6e3b57 allow pulling manifest over authed endpoints 2024-02-07 14:08:03 -08:00
thelamer
c08f7ab515 bump version 2024-02-05 13:59:13 -08:00
thelamer
79988afbbd fallback if binmerge is not supported by the system 2024-02-05 12:00:26 -08:00
thelamer
1d788281cc bump version 2024-01-31 10:59:23 -08:00
thelamer
90a56c94c4 do not attempt to play audio on safari 2024-01-30 17:40:23 -08:00
thelamer
a079add1ec bump version 2024-01-30 11:41:00 -08:00
thelamer
c2a780c49c update arcade videos to be mobile compatible mp4s 2024-01-29 14:56:40 -08:00
thelamer
a6458e550f add atari5200 bios link 2024-01-27 14:38:32 -08:00
thelamer
fbcb869418 add atari5200 support and bump version 2024-01-27 14:24:49 -08:00
thelamer
600b15a14e Add romhacks support and doc it along with a bugfix for people identifying stuff with no metadata at all 2024-01-26 11:40:28 -08:00
thelamer
37d2b56166 update default peer and bump version 2024-01-25 14:53:44 -08:00
thelamer
d9f12b9a1a bump version and add nds metadata 2024-01-21 12:29:35 -08:00
thelamer
27324af975 make sure retroarch is loaded before calling main 2024-01-21 00:50:37 -08:00
thelamer
b9797373ad fix syntax bug 2024-01-13 15:48:21 -08:00
thelamer
651f8ce155 bump version 2024-01-13 15:17:34 -08:00
thelamer
a678343d43 #115 add title to images for mouse hover 2024-01-13 14:00:50 -08:00
thelamer
b0f7a2f7b6 add portal 64 homebrew metadata 2024-01-13 13:46:18 -08:00
thelamer
0eb869fed4 add ref metadata from gr33k on github 2024-01-13 12:15:01 -08:00
Ryan Kuba
1d9b8664bc
Merge pull request #121 from webysther/patch-3
Better logo for space invaders
2024-01-13 12:03:29 -08:00
Ryan Kuba
449d16c987
Merge pull request #122 from webysther/patch-2
Better logo for Bomber Man World (World)
2024-01-13 12:03:20 -08:00
Ryan Kuba
6584c8d4dc
Merge pull request #123 from webysther/patch-1
Fixed doom with correct info
2024-01-13 12:03:11 -08:00
Ryan Kuba
a2b9cf106b
Merge pull request #124 from webysther/patch-4
Metal Slug 7 (U)(XenoPhobia)
2024-01-13 12:03:00 -08:00
Ryan Kuba
e140c2cc10
Merge pull request #125 from webysther/patch-5
Donkey Kong and Tetris HQ logos
2024-01-13 12:02:43 -08:00
Ryan Kuba
fae1f9197c
Merge pull request #128 from webysther/patch-7
Update PSX
2024-01-13 12:02:34 -08:00
Ryan Kuba
c2806a7c78
Merge pull request #126 from webysther/patch-6
More games to SNES
2024-01-13 12:02:25 -08:00
Webysther Sperandio
874a46120d
Update snes.json 2024-01-13 05:18:27 +01:00
Webysther Sperandio
8feda628be
Update snes.json 2024-01-13 05:14:50 +01:00
Webysther Sperandio
80d64355cd
Update nds.json 2024-01-13 05:00:28 +01:00
Webysther Sperandio
fae49c691d
Update psx.json 2024-01-13 04:24:56 +01:00
Webysther Sperandio
873003df4a
Update doom.json 2024-01-13 04:00:50 +01:00
thelamer
e83be0a9ba fix n64 touchpad controls 2024-01-12 16:03:41 -08:00
Webysther Sperandio
2f297564d0
Update nds.json 2024-01-11 04:56:17 +01:00
Webysther Sperandio
f5fa453950
Update snes.json 2024-01-11 04:49:20 +01:00
Webysther Sperandio
28e33df774
Mega Man X (Europe) 2024-01-11 04:35:06 +01:00
Webysther Sperandio
e75b6287e5
Donkey Kong and Tetris HQ logos 2024-01-11 02:57:13 +01:00
Webysther Sperandio
cbe1082d0a
Metal Slug 7 (U)(XenoPhobia) 2024-01-11 02:54:10 +01:00
Webysther Sperandio
50f212fa1a
Update atari2600.json
Better logo for space invaders
2024-01-11 02:45:46 +01:00
Webysther Sperandio
8c72479d3b
Update arcade.json
Better logo of Bomber Man World (World)
2024-01-11 02:44:06 +01:00
Webysther Sperandio
17ff9e79fd
Update doom.json 2024-01-11 02:42:35 +01:00
thelamer
0696142487 bump version and add notes on new n64 core 2024-01-08 13:25:02 -08:00
thelamer
a4ce532fd1 add updated notes on emulators reference 2024-01-07 15:07:48 -08:00
thelamer
44007f3116 bump version, remove ipfs profile upload that does not work, point n64 to new mupen emulator core 2024-01-07 14:54:08 -08:00
thelamer
94b18116db bump version 2023-11-30 12:07:44 -08:00
ryan.kuba
1aa74deed8 minor tweaks for new emulatorjs version that is based on libretro cores for n64 2023-11-30 12:06:33 -08:00
aptalca
c13e408385
Merge pull request #98 from linuxserver/issue-pr2
update issue-pr workflow
2023-08-15 14:23:05 -04:00
aptalca
23fa41ec70
update issue-pr workflow 2023-08-15 09:41:37 -04:00
aptalca
308cd82d39
Merge pull request #86 from linuxserver/issue-pr
add issue-pr workflows
2023-04-07 08:45:35 -04:00
aptalca
342db57497
add issue-pr workflows 2023-04-06 15:35:02 -04:00
Ryan Kuba
b9321545b1
Merge pull request #58 from ltomes/master
Add -j Modifier to unzip commands
2022-06-30 08:18:10 -04:00
Levi
3e17aa9294
Add -j Modifier to unzip commands to prevent directories from being created. 2022-06-20 13:10:23 -05:00
thelamer
d631f7c4a8 bump version 2022-06-16 18:22:50 -04:00
thelamer
e6d3fa72c8 adding merged nes metadata provided by @coffinbearer 2022-06-16 18:06:43 -04:00
Ryan Kuba
cbfc6c6f23
Merge pull request #55 from ltomes/master
Minor spelling corrections in README.md
2022-06-15 14:04:16 -04:00
Levi
1f226be887
Minor spelling corrections 2022-06-14 15:51:37 -05:00
Ryan Kuba
042099abc1
Merge pull request #53 from ltomes/patch-1
Update process_7z to not hash folders
2022-06-09 17:36:12 -04:00
Ryan Kuba
776d87357f
Merge pull request #51 from Emalton/patch-1
Update index.html
2022-06-09 17:35:49 -04:00
Levi Tomes
dbedcf9e81
Updated find, look in temp directory instead of CD
Updated find, look in temp directory instead of CD
2022-06-06 14:01:38 -05:00
Levi Tomes
a24fbf7ad7
Update process_7z to not hash folders
Update process_7z to ignore empty files.

There are cases where `process_7z` is returning a non 0 exit code for valid 7z archives.
My quick fix for this is:
* Do not respect the archive path structure, flatten files in the archive.
* scan for empty files (& folders) after extraction, and delete empty files
* Continue hash as before

This may not work for all roms, some archives may have complicated nested structures I am not aware of.

https://github.com/linuxserver/emulatorjs/issues/52
2022-06-06 13:46:40 -05:00
John P
bf4db4f0f7
Update index.html
Fix typo:
Click one the corresponding scan button above -> Click on the corresponding scan button above
2022-05-30 20:04:20 -05:00
thelamer
520982b81d bump version 2022-05-08 18:11:42 -04:00
Ryan Kuba
ce6111cc29
Merge pull request #45 from XanderStrike/match-filename
Bump matching filename to top of identify rom selector
2022-05-08 18:06:23 -04:00
Ryan Kuba
c8017c636b
Merge pull request #48 from warnwar/master
Fixing some spelling
2022-05-08 18:05:56 -04:00
Dan Torgerson
b86b5651bf fixing spelling 2022-05-08 09:05:02 -05:00
Ryan Kuba
6d120c03ef
Merge pull request #44 from XanderStrike/fix-pipefail
Fix for exit code 141 pipefail in rom scans
2022-05-06 22:26:45 -04:00
Alexander Standke
6c7054bdd6 Bump matching filename to top of identify rom selector
If a rom's filename starts with the exact name of a game, this change
puts that option right at the top of the dropdown so in many cases it's
a single click to confirm.
2022-05-01 13:03:29 -07:00
Alexander Standke
3dce99913e Fix for exit code 141 pipefail in rom scans
When a rom archive contains many files, it can take some time for
`sha1sum` to output shas for every file.

The output of `sha1sum` is piped into an `awk` that reads a single line
and then immediately exits. This causes a race condition: if `sha1sum`
is still writing to the pipe but the reader has exited, the kernel sends
it `SIGPIPE` causing it to exit with code 141.

Since the pipefail option is set, this fails the whole script.

With this change, only the first file in the directory is passed into
`sha1sum`. This should maintain behavior while eliminating the bug.
2022-04-29 18:51:25 -07:00
Ryan Kuba
380673528a
Merge pull request #39 from the-lazy-fox/patch-1
Update index.html
2022-03-15 10:45:04 -07:00
TheLazyFox
e60ef4e1a1
Update index.html 2022-03-10 21:13:39 +01:00
thelamer
e2800587be repoint dev script to forked repo 2022-02-23 12:16:35 -08:00
thelamer
e43884d0f8 bump version 2022-02-19 14:04:52 -08:00
thelamer
f49adf73cf add metadata page to show custom metadata to user 2022-02-19 14:01:21 -08:00
thelamer
58b36683ec add purge and delete options along with a re-scan option to skip scanned roms 2022-02-19 11:57:16 -08:00
thelamer
14e20915e6 add ability to link/unlink and manually identify roms with name 2022-02-18 20:08:58 -08:00
thelamer
83074b098f start of web based metadata editing and custom files 2022-02-18 16:08:40 -08:00
thelamer
29299dd4f3 stash wip development preview logic 2022-02-17 20:51:52 -08:00
thelamer
c3b709d754 stash wip 2022-02-17 17:03:35 -08:00
thelamer
d6c959c837 bump version 2022-02-14 10:09:38 -08:00
thelamer
647a539b48 flag gamestarted on emulatorjs cores 2022-02-14 10:07:57 -08:00
aptalca
0a3b8b995a
Merge pull request #32 from Helvio88/patch-1
Update README.md
2022-02-04 12:30:33 -05:00
Helvio Pedreschi
6f18af3ad9
Update README.md
Typo fix - resubmission
2022-02-04 12:06:40 -05:00
Ryan Kuba
eb12616e25
Merge pull request #30 from Helvio88/patch-2
Update n64.json
2022-02-03 13:57:32 -08:00
Helvio Pedreschi
c1e2100977
Update n64.json
Add "Fire Electric Pen"
2022-02-03 16:52:47 -05:00
thelamer
2f70c0f39c pull out copy and paste code 2022-02-01 12:19:59 -08:00
thelamer
8aefcd43b4 exclude directories from scanning 2022-02-01 12:16:39 -08:00
thelamer
4305f2c785 bump version 2022-01-23 11:22:17 -08:00
thelamer
bf1688dea7 do not allow directories with upload button 2022-01-23 11:21:55 -08:00
thelamer
0d7304309a add drag and drop files/folders support 2022-01-23 11:19:19 -08:00
thelamer
c1a5e9aac8 support multiple file uploads 2022-01-22 13:48:29 -08:00
thelamer
2439a5be1b bump version 2022-01-21 19:59:38 -08:00
thelamer
a65e573397 bump version 2022-01-20 14:15:12 -08:00
thelamer
1343be1f2a do not loop on null gamepads 2022-01-20 11:41:48 -08:00
thelamer
e04062ebb2 bump version 2022-01-19 15:17:47 -08:00
thelamer
74d995f89a pin back to 51.0.0 ipfs-http-client 2022-01-19 15:15:50 -08:00
thelamer
f32c1c5dda pin ipfs api version 2022-01-19 14:36:14 -08:00
thelamer
36b72c458d bump version 2022-01-19 13:50:03 -08:00
thelamer
8ad353ef39 add jump by letter to the menus 2022-01-19 13:48:18 -08:00
Ryan Kuba
03c2bbb3f1
Merge pull request #26 from rrgeorge/gamepadmenu
Gamepadmenu
2022-01-19 11:45:50 -08:00
thelamer
d41e36804d update formatting dont allow gamepad controls on edge chromium 2022-01-19 11:43:07 -08:00
Ryan Kuba
026c2e0fec
Merge pull request #25 from rrgeorge/webapp
Webapp
2022-01-18 21:47:23 -08:00
Robert George
d4e8ddac67 Handle empty list of gamepads 2022-01-18 19:54:18 -08:00
Robert George
7c0ca14354 Adding manifest.json 2022-01-18 19:49:27 -08:00
Robert George
6d6cb79e51 Added gamepad navigation to the menu 2022-01-18 19:44:06 -08:00
Robert George
0ed562a23e Added meta tags and icon for iOS webapp 2022-01-18 19:39:45 -08:00
thelamer
2662a64d74 bump version 2022-01-18 12:33:24 -08:00
thelamer
5a35fc72aa #24 add notes on where to go to report bugs and get support 2022-01-18 12:32:57 -08:00
thelamer
ccb348266e fixes for multizip and multidisk 2022-01-18 12:26:53 -08:00
thelamer
8762578119 add multiwad support for prboom 2022-01-18 11:15:22 -08:00
thelamer
8a90212bd5 add touch input controls 2022-01-17 14:20:15 -08:00
thelamer
62fa7aacfb add touchscreen input on mobile devices 2022-01-17 14:18:46 -08:00
thelamer
4fd9130923 bump version 2022-01-14 14:49:59 -08:00
thelamer
86cbc01522 add profile concept with server side endpoint 2022-01-14 14:14:29 -08:00
Ryan Kuba
e4142a174e
Merge pull request #21 from rrgeorge/rrgeorge-patch-cue-loading
Remove code that prevents cue from loading
2022-01-09 13:36:16 -08:00
thelamer
fa4ed179c4 use seperate variable to tell the emulator to load a cue file when needed 2022-01-09 13:32:59 -08:00
Robert R George
d4b89e8918
Remove code that prevents cue from loading
This block seems to prevent the cue file from loading.
2022-01-09 12:38:53 -08:00
thelamer
e0f64eece4 remove log 2022-01-09 12:36:18 -08:00
thelamer
7b64c351b7 bump version and use 1st member of array 2022-01-09 12:34:43 -08:00
Ryan Kuba
0de3699ca6
Merge pull request #20 from rrgeorge/patch-1
Needs polyfill for Array.at() in Safari
2022-01-09 12:28:21 -08:00
thelamer
22972007fc swap to slice 2022-01-09 11:44:01 -08:00
Robert R George
f4d24d2af0
Needs polyfill for Array.at() in Safari
Array.at() is new and not fully supported across browsers. This polyfill should resolve that.
2022-01-09 07:54:43 -08:00
thelamer
14ad375a59 purge docs dir from emulatorjs repo 2022-01-08 15:38:22 -08:00
thelamer
091315f216 typo 2022-01-08 15:36:38 -08:00
thelamer
d0bf858a86 add delete function and link to file browser from frontend 2022-01-08 15:32:40 -08:00
thelamer
e5e478c3ea add all funtionality to file browser and style it a bit 2022-01-08 13:53:26 -08:00
thelamer
2285047e4e add ability to download a full backup 2022-01-07 20:27:07 -08:00
thelamer
e923fb4b36 add basic downloading and navigation to file browser 2022-01-07 18:51:17 -08:00
thelamer
f769d5b4ca skeleton file browser 2022-01-07 14:14:36 -08:00
thelamer
3bfd9bc79f add a simple sleep statement for threaded emulators 2022-01-07 10:48:15 -08:00
thelamer
98c7320fb0 bump version 2022-01-06 15:07:10 -08:00
thelamer
20dde69386 add notes on threaded saturn 2022-01-06 15:06:22 -08:00
thelamer
1fd899411b add a bunch of nds metadata linking 2022-01-05 15:42:44 -08:00
thelamer
fc746b5241 bump version 2022-01-05 07:27:23 -08:00
thelamer
ea608bc3e7 update default files for nds changes 2022-01-05 07:27:06 -08:00
thelamer
3563002a5e add nds video previews 2022-01-04 22:43:32 -08:00
thelamer
f92ca43ff6 add nds logos 2022-01-04 12:37:44 -08:00
thelamer
5ba579779e bump version 2022-01-04 12:09:20 -08:00
thelamer
d83c1df092 add notes on threaded emulation and add needed headers to node in dev mode 2022-01-04 11:26:55 -08:00
thelamer
5a74df2c1a bug in dev mode for combined metadata 2021-12-27 20:28:51 -08:00
thelamer
7adab856b1 bump version 2021-12-27 13:30:31 -08:00
thelamer
f7270fe1e2 add logo metadata for many systems 2021-12-27 13:20:15 -08:00
thelamer
f91e765945 no superscroll for mouse 2021-12-14 11:45:40 -08:00
thelamer
d8187aede1 add notes about navigation and browser support 2021-12-13 12:37:06 -08:00
thelamer
6846dca619 remove test 2021-12-12 15:09:07 -08:00
thelamer
3c49f952ee return uint8 arrays for files without a length found 2021-12-12 15:08:07 -08:00
thelamer
a2056142f3 make system folder even without bios files 2021-12-11 13:24:56 -08:00
thelamer
11877e810f add metdata linking 2021-12-09 11:57:56 -08:00
thelamer
abad3c60b1 update error in metdata 2021-12-08 22:50:09 -08:00
thelamer
286246f35f add a bunch of rom reference data for similar roms 2021-12-08 20:31:16 -08:00
thelamer
9ca44bfa3a bump version 2021-12-08 12:57:57 -08:00
thelamer
d33d2bf2fd add right arrow speedup for arrow down and arrow up scrolling 2021-12-08 12:57:28 -08:00
thelamer
87f1dee870 speed up touch pan scrolling a bit and mouse wheel events 2021-12-08 12:04:06 -08:00
thelamer
2f8e5daa02 add ability to purge roms from configs that dont have art assets 2021-12-08 11:26:37 -08:00
thelamer
090df82d0b make it so if NEStool fails we just take a shasum of what is there 2021-12-08 11:03:55 -08:00
thelamer
65c95d815d weird bugfix for safari 2021-12-06 19:50:04 -08:00
thelamer
2ac417c52e update platform detection 2021-12-06 16:35:01 -08:00
thelamer
2086a2483c adding safari support and reworking audio init for new cores, resize canvas to window 2021-12-06 16:02:53 -08:00
thelamer
b5c6e0ff52 support running cores built with later emsdk versions 2021-12-02 16:19:29 -08:00
thelamer
a402615e69 bump version 2021-12-02 12:10:23 -08:00
thelamer
5689aa158f remove deps from libretro 2021-12-02 12:01:30 -08:00
thelamer
9faa6ad723 stop initializing the fs object for emscripten compatibility 2021-11-29 13:02:32 -08:00
thelamer
9f2e5972dd bump version 2021-11-26 12:59:28 -08:00
thelamer
e19eace32f update dev setup for new assets 2021-11-26 12:35:05 -08:00
thelamer
eb34a9466d use variable roms directory so core specific settings stay that way 2021-11-25 18:38:44 -08:00
thelamer
e41c5f6bdd notes on multi disk saturn 2021-11-23 11:01:52 -08:00
thelamer
44326348b4 bump version 2021-11-23 09:55:15 -08:00
thelamer
0965bbc11d bump version 2021-11-23 09:54:52 -08:00
thelamer
6592d1846b automatically manage items in the main landing page if user is using rom scanning 2021-11-22 16:00:40 -08:00
thelamer
568d235c1b do not create config file if exists, overlay is non functional in web no reason to have those assets 2021-11-22 15:04:04 -08:00
thelamer
8ba4a7d40d corner case scanning for saturn 2021-11-19 14:30:53 -08:00
thelamer
ef6c838fb6 pin retroarch version 2021-11-19 12:33:25 -08:00
thelamer
ec235acf0e fix metdata bug 2021-11-18 20:44:59 -08:00
thelamer
168553701c dont mount roms directory and no percentage for chunked downloads 2021-11-18 19:53:12 -08:00
thelamer
219a9e0c93 update game downloading logic to be multi part 2021-11-18 12:53:22 -08:00
thelamer
c824f31225 update metadata and add support for larger chds in xbox mode for libretro 2021-11-17 15:49:50 -08:00
62 changed files with 201607 additions and 169291 deletions

View 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
View 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

View File

@ -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.

View 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
View 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

View 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

View File

@ -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
View 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()">&uarr; Push to Server</button>
<button class="right" onclick="pullProfile()">&darr; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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
View 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();
}

View File

@ -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
View 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);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

File diff suppressed because one or more lines are too long

17
frontend/manifest.json Normal file
View 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": "/"
}

View File

@ -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
View File

@ -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
}
}));

File diff suppressed because it is too large Load Diff

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
View 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

View File

@ -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"

View File

@ -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"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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)"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}
}

View File

@ -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)"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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);

View File

@ -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;
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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 \