Author SHA1 Message Date
Pedro Santos b0181457b8 Merge pull request #34 from s0liton/readme-update
Plug the YT channel for teh gains... /s
2026-10-25 16:15:44 -04:00
Pedro Santos 69cb1c7ce3 Plug the YT channel for teh gains... /s
Added information about emulator profiles and development progress.
2026-10-25 11:18:31 -04:00
Pedro Santos 83ee938528 Merge pull request #33 from s0liton/recompstore-qr
README: RecompStore QR code
2026-10-25 01:26:08 -04:00
Pedro Santos 2c7b069290 Fixed recompStore qr code 2026-10-25 01:20:13 -04:00
Pedro Santos f2c0c9bbd1 Add files via upload 2026-10-25 01:16:59 -04:00
Pedro Santos 804dea1e50 Merge pull request #31 from s0liton/readme-optimization
Revised readme for clarity and simplification
2026-10-24 01:11:06 -04:00
Pedro Santos 833307403a Revised readme for clarity and simplification 2026-10-24 01:04:43 -04:00
Pedro Santos 63e5884f68 Merge pull request #26 from s0liton/release/v1.3.0
v1.3.0: PS4 memory fixes
2026-10-22 20:29:18 -04:00
Pedro Santos a1981fa5fb Merge pull request #25 from s0liton/release/v1.3.0
v1.3.0
2026-10-22 18:55:08 -04:00
15 changed files with 438 additions and 834 deletions

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@
CHDR_SRCS := $(filter-out %microflac.cpp,$(wildcard third_party/libchdr/src/*.c)) \
third_party/libchdr/deps/lzma/src/LzmaDec.c third_party/libchdr/deps/zstd/zstddeclib.c
SRCS := src/main.c src/util.c src/http.c src/romm.c src/config.c src/state.c \
src/profiles.c src/sync.c src/watch.c src/detect.c src/ra2.c src/playlist.c src/autostart.c src/memcard.c src/zip.c src/bundle.c src/gcmem.c src/statefmt.c src/events.c src/gameid.c src/covers.c src/library.c src/firmware.c src/pair.c src/web.c src/update.c src/presets.c src/jsonlean.c \
src/profiles.c src/sync.c src/watch.c src/detect.c src/ra2.c src/playlist.c src/autostart.c src/memcard.c src/zip.c src/bundle.c src/gcmem.c src/statefmt.c src/events.c src/gameid.c src/covers.c src/library.c src/firmware.c src/pair.c src/web.c src/update.c src/jsonlean.c \
third_party/cJSON.c third_party/md5.c third_party/monocypher.c third_party/monocypher-ed25519.c \
$(CHDR_SRCS)
HDRS := $(wildcard src/*.h) third_party/cJSON.h third_party/md5.h third_party/monocypher.h third_party/monocypher-ed25519.h
+65 -111
View File
@@ -28,92 +28,24 @@ A [RomM](https://romm.app) companion client for jailbroken PS5 consoles (etaHEN
- **Easter Eggs**
- Check in the Settings, scroll the settings list, see if you find the X. This **hint** won't be here forever.
## Supported Emulators and Games
- RommPS has built-in profiles for all known emulators on the PS5, and will set the correct default game, save/save state, and bios directories for them. Since there is a huge influx of new emulators popping up, I will be doing my very best to keep up with them and add profiles accordingly.
- You can also manually set the directories for each emulator/platform via the PS5 app or or the Web UI. **RommPS can support ANY emulator truly.**
### Note
- RommPS relies on your emulator **NOT** being sandboxed. Some emulators require a separate payload that must be launched to be able to access outside of the sandbox, so make sure you've followed their instructions to properly install. Sandboxed emulators will not be able to read games or save files.
| Emulator | Systems | Game Downloads | Saves | Comments |
| --- | --- | --- | --- | --- |
| [RetroArch](https://github.com/mihawk-99/PS5_RetroArch) | NES, Famicom Disk System, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis/Mega Drive, Master System, Game Gear, Sega CD, SG-1000, arcade (FinalBurn Neo), PS2 (PS5SX2 on RetroArch 2.0, LRPS2 before it), PSP (PPSSPP), GameCube and Wii (Dolphin). Any other core you install for a system RomM knows (PS1, N64, Dreamcast, Saturn, 3DS and friends) gets picked up too | Full Support | Full Support | - |
| [PSXS5](https://github.com/SynoPiia/PSXS5) | PS1 | Full Support | One memory card per game, and states | States work with RomM's web player (PCSX-ReARMed or Beetle, whichever PSXS5 is set to) |
| [SwanStationPS5](https://github.com/darkxex/SwanStationPS5) | PS1 | Full Support | One memory card per game, and states | Plays PSXS5's games, keeps its own saves in `/data/SwanStationPS5`. Its states only load in SwanStationPS5 |
| PSSwanStation | PS1 | Full Support | One memory card per game, and states | Its states only load in PSSwanStation |
| [PS5SX2](https://github.com/Swordpdf/PS5SX2) | PS2 | Full Support | Its two shared memory cards, backed up to RomM (5.3 or newer) | - |
| [PS5N64](https://github.com/Globaleliteee/PS5N64) | N64 | Full Support | Full Support | - |
| [PS5NES](https://github.com/Globaleliteee/PS5NES) | NES | Untested | Untested | Preset uses `/data/PS5NES/games` and `/data/PS5NES/saves`. GH page gone |
| PSFlyCast | Dreamcast | Full Support | Each game's own VMU; the shared VMUs are backed up as RomM memory cards | Save states don't sync yet |
| PS64 | Commodore 64 | Full Support | Save disks | Games save into their own disk images. No save states |
| PS5CEMU-HAR | Wii U, 3DS, DS | Full Support | Wii U and 3DS saves, 3DS states | 3DS states only load in PS5CEMU-HAR. DS is games and BIOS files only, for now |
| [Porpoise](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5) | Wii/GameCube | Full Support | N/A | Couldn't exit sandbox w/o crash. [Issue](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/issues/18) reported. Downloading games should work if the emulator is actually working for you.|
| XPSemu | Xbox | Full Support | Not synced | Games and BIOS files only: its saves live inside its hard disk image |
| Seedee5 | Philips CD-i | Full Support | Not synced | Games and BIOS files only: it keeps saves as folders, and only after a clean exit |
| [ProsperoEden](https://github.com/blackbearreloaded/ProsperoEden) | Switch | Full Support | Not synced | Games and keys only: saves are kept per Switch user profile |
| [PS5X360](https://github.com/BrinooTk/PS5X360) | Xbox 360 | Manual Support | Researching | - |
| Mednafen | PS1, NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis/Mega Drive, Master System, Game Gear, PC Engine, Neo Geo Pocket, WonderSwan, Virtual Boy, Lynx | Full Support | Full Support | Built-in profile, off until you turn it on |
| PS2 ISOs (for PS2 FPKG tools) | PS2 | Full Support | Not synced | Downloads PS2 ISOs to a USB drive (`/mnt/usb0/ps2`) for PS2 FPKG tools. Off until you turn it on |
To give some meaning to the table above. **Full support** means RommPS can detect and properly set game/save directories for that emulator. **Manual Support** means you can use it to sync games and/or saves, but you'll need to point it to the directories yourself. This typically means that full support is coming shortly. Broken or N/A means there was a problem keeping us from sync games and saves, or the emulator just fall out doesn't work or crashes. **Researching** means I am seeing what is possible when it comes to that sync type for the given emulator or game system.
### PS4 (experimental) ###
| Emulator | Systems | Saves | Comments |
| --- | --- | --- | --- |
| RetroArch | The RetroArch systems above, wherever the PS4 build has the core | Saves and states | - |
| pSNES | SNES | Saves and states | - |
| pNES | NES, Famicom Disk System | Saves and states | - |
| pGEN | Genesis/Mega Drive, Master System, Game Gear, Sega CD, SG-1000 | Saves and states | - |
| pGBA | Game Boy Advance, Game Boy Color, Game Boy | Saves and states | Saves and states share its saves folder |
| pFBN | Arcade (FinalBurn Neo), Neo Geo Pocket (Color), ColecoVision, MSX, ZX Spectrum, SuperGrafx, Channel F | Saves and states | Arcade saves are the games' own memory (EEPROMs), backed up as they are |
| mGBA | Game Boy Advance, Game Boy Color, Game Boy | Saves and states | - |
| SNESUI | SNES | Saves and states | - |
| EmuSuNi | SNES, NES, N64 | Saves and states | Set its N64 folder to `/data/EmuSuNi/n64` in its settings |
| NPS64 | N64 | Saves and states | `.z64` dumps |
| DolphinPS4 | GameCube, Wii | Saves and states | - |
| Resonance | Dreamcast | Each game's own VMU, and states; the shared VMUs are backed up as RomM memory cards | - |
| PS4SX2 | PS2 | Its two shared memory cards, backed up to RomM (5.3 or newer) | No save states yet |
| Mednafen, PS2 ISOs | As on the PS5 | As on the PS5 | Built-in profiles, off until you turn them on |
### Save states
Save states only load in the emulator core that made them, sometimes only in the same version of it. RommPS backs every state up to RomM, and with **Save states: Back up and restore** in Settings it brings one down only to an emulator that can load it:
- **RetroArch:** states from the same core, from RomM's web player or another RetroArch.
- **PSXS5:** PCSX-ReARMed or Beetle states, whichever core PSXS5 is set to. They're converted to and from the web player's format.
- **SwanStationPS5, PS5N64:** only their own states.
A state from elsewhere never replaces one of yours: it goes into the next free slot.
## PS4/PS5 Games
If you are interested in downloading PS4 and PS5 games from your RomM server, RomM has direct FPKGi support, simply add it as a library. This sync client does NOT handle PS4/PS5 save syncing at this time, but is being researched for potential future inclusion.
## Requirements
- A PS5 running etaHEN or onionHEN, or a PS4 running GoldHEN
- RomM 5.2.0 or newer (5.3.0 if you want to do shared memory card syncing instead of per-game)
- RetroArch installed
- RomM 5.2.0 or newer (may work on older versions)
- Whichever other emulators you want
## Web UI
- RommPS exposes a separate Web UI at http://your-ps5-ip:8780 which allows you to access most of the features present in the web app, but can make things like sync profiled editing, and adding multiple downloads of games easier from your computer instead.
## Install
### Install via ProsperoStore
### PS5
![ProsperoStoreQRCode](assets/prospero-store-rommps.png)
Install **RommPS** from [ProsperoStore](https://homebrew.page) and open it. It'll set up the background daemon, and take you through a setup wizard. That's it: RommPS carries the RomM Sync payload and starts it for you. From then on it starts with your HEN, so after a jailbreak or waking the console it's ready to go.
| ProsperoStore | RecompStore |
| :---: | :---: |
| <img src="/assets/prospero-store-rommps.png" width="300" alt="ProsperoStore Install QR"> | <img src="/assets/recomp-store-rommps.png" width="300" alt="RecompStore Install QR"> |
### Manual Install
Download `PPSA76677.zip` from the releases page, unzip it and copy the `PPSA76677` folder to `/data/homebrew/` on the console. Launch RommPS and it will deploy the background service, and set itself to autostart with etaHen or onionHen.
Not using the store? Download `PPSA76677.zip` from the releases page, unzip it and copy the `PPSA76677` folder to `/data/homebrew/` on the console. Just run the RommPS app and it'll copy the background service for you.
## Payload Only - No PS5 App
If you'd rather run just the payload, without the app and adminster via the Web UI:
If you'd rather run just the payload, without the app:
1. Download the latest `romm-sync.elf` from the releases page.
2. Send it to the console (or use whichever favorite tool you wish):
```sh
@@ -123,28 +55,30 @@ If you'd rather run just the payload, without the app and adminster via the Web
### PS4
> **Experimental.** The PS4 build has only been tested with RetroArch so far. Standalone emulators may work, but haven't been confirmed. I spent all my focus on the PS5 build, so I didn't get to test as much, so consider it unstable and there may be gremlins for now.
> **Experimental.** The PS4 is currently considered experimental
Needs GoldHEN 2.4b18.5 or newer, whose payload menu runs ELF payloads.
R
1. Download the latest `romm-sync-ps4.pkg` from the releases page and install it with your package installer.
2. Open **RomM Sync** from the home screen. It copies its payload, `romm-sync-ps4.elf`, to `/data/payloads/`.
2. Open **RommPS** from the home screen. It copies its payload, `romm-sync-ps4.elf`, to `/data/payloads/`.
3. In GoldHEN Settings, open the payloader, pick `romm-sync-ps4.elf` and run it. Add it to the AutoRun queue there and it starts on every jailbreak.
4. Open **RomM Sync** again: it shows the web UI. It's also at `http://<ps4-ip>:8780` from another device.
4. Open **RommPS** again: it shows the web UI. It's also at `http://<ps4-ip>:8780` from another device.
The app only installs the payload and shows the web UI; the payload does the syncing, in the background, whether or not the app is open. Without the package, copy `romm-sync-ps4.elf` to `/data/payloads/` yourself (GoldHEN's FTP server, or `make install-ps4 PS4_HOST=<ps4-ip>`).
Don't send the payload to GoldHEN's network loader on port 9090: GoldHEN 2.4 crashes on ELFs sent there. Make sure the console's date and time are set (GoldHEN can keep them updated), or HTTPS won't work. RetroArch is found in `/data/retroarch`, with its cores in `/data/self/retroarch/cores`, as its `retroarch.cfg` says.
**Note:** Don't send the payload to GoldHEN's network loader on port 9090: GoldHEN 2.4 crashes on ELFs sent there. Make sure the console's date and time are set (GoldHEN can keep them updated), or HTTPS won't work.
### How it runs
RommPS starts with your HEN, and setup turns this on for you. Both HENs start `payloads/romm-sync.elf` when `romm-sync.elf.auto_start` sits next to it, and RommPS puts both in every HEN's folder on the console (`/data/etaHEN/payloads` and `/data/OnionHEN/payloads`), so switching HENs keeps working. If a payload manager like pldmgr has its own copy, that copy is kept up to date too. If you build it yourself, `make install PS5_HOST=<ps5-ip>` copies it over FTP (add `PS5_FTP=ftp://<ps5-ip>:1337 PS5_HEN_DIR=/data/OnionHEN` for onionHEN's FTP plugin).
## Updates
On the PS5, just update RommPS: from ProsperoStore (or other homebrew storefronts eventually), or by copying the new zip's folder over the old one. The background service comes with it, and RommPS swaps it in the next time you open it, once nothing is syncing or downloading. An older version never replaces a newer one.
- On the PS5, just update RommPS: from ProsperoStore (or other homebrew storefronts eventually), or by copying the new zip's folder over the old one. The background service comes with it, and RommPS swaps it in the next time you open it, once nothing is syncing or downloading. An older version never replaces a newer one.
- On the PS4, the payload updates itself from Settings.
On the PS4, the payload updates itself from Settings.
## Setup Wizard
## Setup
The setup walks you through five steps:
The setup wizard walks you through five steps:
1. Connecting to your server.
2. Approving the console in RomM, by scanning a QR code or entering a code.
@@ -152,39 +86,55 @@ The setup walks you through five steps:
4. Checking what the first sync will do.
5. Choosing when to sync in the background.
**Back up your saves before the first sync.** Copy your emulator save folders to a computer or USB drive. RomM Sync keeps a local copy before replacing any file, but your own backup is the safest option.
**Back up your saves before the first sync.** Copy your emulator save folders to a computer or USB drive. RommPS keeps a local copy before replacing any file, but your own backup is the safest option.
## Emulators
RommPS has built-in profiles for most known emulators on the PS5, and will set the correct default game, save/save state, and bios directories for them. Since there is a huge influx of new emulators popping up, I will be doing my very best to keep up with them and add profiles accordingly.
Note: RommPS relies on your emulator **NOT** being sandboxed. Some emulators require a separate payload that must be launched to be able to access outside of the sandbox, so make sure you've followed their instructions to properly install. Sandboxed emulators will not be able to read games or save files.
## Other "Memory Card" Consoles
PSP, GameCube and Wii saves are synced as one zip per game, the same format other RomM clients use. Depending on the client, they may or may not support unzipping the saves (I don't truly know), but this client does!
> The emulators listed below are those I've tested and have created profiles for to auto-detect them. You can also manually set the directories for each emulator/platform via the PS5 app or or the Web UI; you can use whichever emulators you wish.
My opinion? One card per game is better since 8MB memory cards fill up quick.
### PS5
| Emulator | Systems | Game Downloads | Saves | Comments |
| --- | --- | --- | --- | --- |
| [RetroArch](https://github.com/mihawk-99/PS5_RetroArch) | NES, Famicom Disk System, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis/Mega Drive, Master System, Game Gear, Sega CD, SG-1000, arcade (FinalBurn Neo), PS2 (LRPS2), PSP (PPSSPP), GameCube and Wii (Dolphin) | Full Support | Full Support | - |
| [PSXS5](https://github.com/SynoPiia/PSXS5) | PS1 | Full Support | One memory card per game, and states | States work with RomM's web player (PCSX-ReARMed or Beetle, whichever PSXS5 is set to) |
| [SwanStationPS5](https://github.com/darkxex/SwanStationPS5) | PS1 | Full Support | One memory card per game, and states | Plays PSXS5's games, keeps its own saves in `/data/SwanStationPS5`. Its states only load in SwanStationPS5 |
| [PS5SX2](https://github.com/Swordpdf/PS5SX2) | PS2 | Full Support | Its two shared memory cards, backed up to RomM (5.3 or newer) | - |
| [PS5N64](https://github.com/Globaleliteee/PS5N64) | N64 | Full Support | Full Support | - |
| [PS5NES](https://github.com/Globaleliteee/PS5NES) | NES | Untested | Untested | Preset uses `/data/PS5NES/games` and `/data/PS5NES/saves`. GH page gone |
| [Porpoise](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5) | Wii/GameCube | Full Support | N/A | Couldn't exit sandbox w/o crash. [Issue](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/issues/18) reported. Downloading games should work if the emulator is actually working for you.|
| [ProsperoEden](https://github.com/blackbearreloaded/ProsperoEden) | Switch | Manual Support | Researching | - |
| [PS5X360](https://github.com/BrinooTk/PS5X360) | Xbox 360 | Manual Support | Researching | - |
### PS4
| Emulator | Systems |
| --- | --- |
| RetroArch | The RetroArch systems above, wherever the PS4 build has the core |
| pSNES, pNES, pGEN, pGBA, pFBN | SNES; NES; Genesis, Master System, Game Gear, Sega CD, SG-1000; GBA, GBC, GB; arcade |
### Save states
Some users have recently reported issues with being able to pull down save states from their servers. They don't see them on the destination save folder for that emulator, or if they do, they don't work. Save states typically cannot be used across different emulators (cores), and even within the same core, the definition of a save state may different with the file version, and version of the app. In my opinion, save states should be for quick resume or to save you when you forgot to save your game, so they truly don't need to be synced across devices.
## PS4/PS5 Games
If you are interested in downloading PS4 and PS5 games from your RomM server, RomM has direct FPKGi support, simply add it as a library. This sync client does NOT handle PS4/PS5 save syncing at this time, but is being researched for potential future inclusion.
## Web UI
RommPS exposes a separate Web UI at http://your-ps5-ip:8780 where you can access all the same features present in the native app.
## Emulator Profiles
I've included direct support for most of the emulators that are out right now, except for the ones that won't even launch... you can sync games, states, and saves anywhere you want, but the presets should do it for most people.
Settings in the web UI has a JSON editor for emulator profiles, and a table showing which folders are used for each platform. A profile lists an emulator's folders and the RomM platforms it plays:
```json
{
"id": "retroarch",
"name": "RetroArch",
"root": "/data/homebrew/PPSA99169",
"retroarch_cfg": "{root}/config/retroarch.cfg",
"rom_dir": "{root}/content/{platform}",
"save_dir": "{root}/savefiles",
"state_dir": "{root}/savestates",
"bios_dir": "{root}/system",
"save_exts": ".srm,.sav",
"platforms": [{ "romm": ["snes", "sfam"], "dir": "snes", "emulator": "snes9x", "core_name": "Snes9x" }]
}
```
I've included direct support for most of the emulators that are out right now, except for the ones that won't even launch... you can sync games, states, and saves anywhere you want, but the presets should do it for most people. You can also configure the paths for all platforms manually. The web UI has a JSON editor for your emulator profiles.
## Building
Both consoles build from the same code in `src/`. What differs lives in `platform/<console>/`: `platform.c` (the `src/platform.h` interface), the make rules and the Docker SDK image. `platform/console/` has the parts the PS4 and PS5 share, and `platform/host/` is the development build.
```sh
make host tools # macOS or Linux build for development, plus tools/release-sign
tools/ps5-build.sh # PS5 build in Docker, output in build/ps5/
@@ -194,7 +144,11 @@ tests/e2e.sh # end-to-end tests against a mock RomM server
tests/autostart.sh # autostart, version checks and HEN folders
```
Both consoles build from the same code in `src/`. What differs lives in `platform/<console>/`: `platform.c` (the `src/platform.h` interface), the make rules and the Docker SDK image. `platform/console/` has the parts the PS4 and PS5 share, and `platform/host/` is the development build.
## Watch Development Progress
I've started streaming the development of this app as well as other side projects on my YouTube channel. Nothing serious, but you can catch me live streaming. ;)
[My Channel](https://www.youtube.com/@soliton_radar)
## P.S to the industry
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+2 -2
View File
@@ -98,13 +98,13 @@ Games and BIOS files only: saves live inside its hard disk image.
### PS5CEMU-HAR
Wii U (Cemu), 3DS (Azahar) and DS (melonDS). Saves sync for all three; 3DS and DS save states load only in PS5CEMU-HAR.
Wii U (Cemu), 3DS (Azahar) and DS (melonDS). Wii U and 3DS saves sync; 3DS save states load only in PS5CEMU-HAR. DS: games and BIOS files only for now.
| System | Games | Saves | Save files | Saved as | States | State files | Memory cards |
|---|---|---|---|---|---|---|---|
| wiiu | `{root}/games` | `{rom_dir}` | a zip per game (wiiu), from `{root}/mlc01/usr/save` | | `{rom_dir}` | `.state*` | |
| 3ds | `{root}/azahar/games` | `{rom_dir}` | a zip per game (3ds), from `{root}/azahar/sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/title` | | `{root}/azahar/states` | `.cst` | |
| nds | `{root}/melonds/games` | `{root}/melonds/saves` | `.sav` | `{rom_stem}{ext}` | `{root}/melonds/states` | `.ml*` | |
| nds | `{root}/melonds/games` | not synced | | | not synced | | |
### ProsperoEden
+159 -164
View File
@@ -1,165 +1,160 @@
{
"format": 1,
"version": "2026.10.23.1",
"console": "ps4",
"emulators": [
{
"id": "psnes", "name": "pSNES", "title_ids": ["PSNE00001"], "detect": ["/data/psnes"],
"profile": {
"root": "/data/psnes", "rom_dir": "{root}/roms", "save_dir": "{root}/sram", "bios_dir": "{root}/bios",
"save_exts": ".srm",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"firmware": {"from": {"satellaview": ["BS-X.bin"], "sufami-turbo": ["STBIOS.bin"]}},
"platforms": [{"romm": ["snes", "sfam"], "dir": "snes"}]
}
},
{
"id": "pnes", "name": "pNES", "title_ids": ["PNES00001"], "detect": ["/data/pnes"],
"profile": {
"root": "/data/pnes", "rom_dir": "{root}/roms", "save_dir": "{root}/save", "bios_dir": "{root}/bios",
"save_exts": ".sav",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"firmware": {"dir": "{root}"},
"platforms": [{"romm": ["nes", "famicom"], "dir": "nes"}, {"romm": ["fds"], "dir": "fds"}]
}
},
{
"id": "pgen", "name": "pGEN", "title_ids": ["PGEN00001"], "detect": ["/data/pgen"],
"profile": {
"root": "/data/pgen", "rom_dir": "{root}/{platform}", "save_dir": "{root}/rams", "bios_dir": "{root}/bios",
"save_exts": ".srm",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"platforms": [
{"romm": ["genesis", "genesis-slash-megadrive", "megadrive"], "dir": "megadrive"},
{"romm": ["sms", "mastersystem"], "dir": "mastersystem"},
{"romm": ["gamegear", "gg"], "dir": "gamegear"},
{"romm": ["segacd", "sega-cd"], "dir": "megacd", "save_exts": ".brm"},
{"romm": ["sg1000"], "dir": "sg1000"}
]
}
},
{
"id": "pgba", "name": "pGBA", "title_ids": ["PGBA00001"], "detect": ["/data/pgba"],
"note": "Saves and states share the saves folder.",
"profile": {
"root": "/data/pgba", "rom_dir": "{root}/{platform}", "save_dir": "{root}/saves", "bios_dir": "{root}/bios",
"save_exts": ".sav",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"platforms": [
{"romm": ["gba"], "dir": "gba"},
{"romm": ["gbc"], "dir": "gbc"},
{"romm": ["gb"], "dir": "gb"}
]
}
},
{
"id": "pfbn", "name": "pFBN", "title_ids": ["PFBN00001"], "detect": ["/data/pfba"],
"note": "Arcade saves are the games' own memory (eeproms), backed up as they are.",
"profile": {
"root": "/data/pfba", "rom_dir": "{root}/{platform}", "save_dir": "{root}/eeproms", "bios_dir": "{root}/arcade",
"save_exts": ".fs",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"firmware": {"dir": "{rom_dir}"},
"platforms": [
{"romm": ["arcade", "fbneo", "neogeoaes", "neogeomvs", "cps1", "cps2", "cps3"], "dir": "arcade"},
{"romm": ["neo-geo-pocket-color", "neo-geo-pocket", "ngpc", "ngp"], "dir": "ngp"},
{"romm": ["colecovision"], "dir": "coleco"},
{"romm": ["msx"], "dir": "msx"},
{"romm": ["zxs", "zx-spectrum"], "dir": "spectrum"},
{"romm": ["supergrafx"], "dir": "sgx"},
{"romm": ["channel-f", "fairchild-channel-f"], "dir": "channelf"}
]
}
},
{
"id": "emusuni", "name": "EmuSuNi", "title_ids": ["EMSN00001"], "detect": ["/data/EmuSuNi"],
"note": "Set EmuSuNi's N64 folder to /data/EmuSuNi/n64 in its settings.",
"profile": {
"root": "/data/EmuSuNi", "bios_dir": "{root}/system",
"save_exts": ".srm",
"state_exts": ".state*", "state_format": "raw", "state_slot0": ".state1",
"platforms": [
{"romm": ["snes", "sfam"], "dir": "snes", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/states"},
{"romm": ["nes", "famicom"], "dir": "nes", "rom_dir": "{root}/nes", "save_dir": "{root}/saves/nes", "state_dir": "{root}/states/nes"},
{"romm": ["n64"], "dir": "n64", "rom_dir": "{root}/n64", "save_dir": "{root}/saves/n64", "state_dir": "{root}/states/n64", "save_format": "n64-srm", "cards": {"default": "per_game", "modes": "per_game"}}
]
}
},
{
"id": "dolphinps4", "name": "DolphinPS4", "title_ids": ["DLPH00010"], "detect": ["/data/DolphinPS4"],
"note": "Dolphin, with its user folder in {root}/User. States are named after the disc's id.",
"profile": {
"root": "/data/DolphinPS4", "rom_dir": "{root}/games", "save_dir": "{root}/User",
"state_dir": "{root}/User/StateSaves", "state_exts": ".s0*", "state_format": "raw",
"state_stems": "{game_id}", "state_name": "{game_id}{ext}",
"firmware": {"dir": "{root}/User/GC", "only_named": true, "names": {"b17148254a5799684c7d783206504926": "USA/IPL.bin", "fc924a7c879b661abc37cec4f018fdf3": "USA/IPL.bin", "019e39822a9ca3029124f74dd4d55ac4": "USA/IPL.bin", "db92574caab77a7ec99d4605fd6f2450": "EUR/IPL.bin", "339848a0b7c2124cf155276c1e79cbd0": "EUR/IPL.bin", "0cdda509e2da83c85bfe423dd87346cc": "EUR/IPL.bin"}},
"platforms": [
{"romm": ["ngc", "gamecube"], "dir": "gamecube", "emulator": "dolphin", "save_layout": "gci", "bundle_dirs": "{root}/User/GC", "cards": {"default": "per_game", "modes": "per_game"}},
{"romm": ["wii"], "dir": "wii", "emulator": "dolphin", "save_layout": "wii", "bundle_dirs": "{root}/User/Wii"}
]
}
},
{
"id": "resonance", "name": "Resonance", "title_ids": ["DCST00001"], "detect": ["/data/Resonance"],
"note": "Each game's own memory card syncs as its save; the shared ones are backed up as RomM memory cards.",
"profile": {
"root": "/data/Resonance", "rom_dir": "{root}/games", "save_dir": "{root}/data", "bios_dir": "{root}/data",
"save_exts": ".bin", "disc_id": "dreamcast",
"save_stems": "{game_id}_vmu_save_A1", "save_name": "{game_id}_vmu_save_A1{ext}",
"memcards": "{root}/data/vmu_save_A1.bin,{root}/data/vmu_save_B1.bin,{root}/data/vmu_save_C1.bin,{root}/data/vmu_save_D1.bin",
"state_dir": "{root}/data", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}_1,{rom_stem}_2,{rom_stem}_3", "state_name": "{rom_stem}{ext}",
"firmware": {"names": {"e10c53c2f8b90bab96ead2d368858623": "dc_boot.bin"}, "from": {"arcade": ["naomi.zip", "naomi2.zip", "awbios.zip"]}},
"platforms": [{"romm": ["dc", "dreamcast"], "dir": "dreamcast", "save_format": "dc-vmu", "cards": {"default": "per_game", "modes": "per_game,shared", "change": "Turn on Own memory card for each game in its settings (players 2 to 4 always share theirs)", "setting": {"file": "{root}/emu.cfg", "key": "PerGameVmu", "per_game": "yes,true", "shared": "no,false"}}}]
}
},
{
"id": "ps4sx2", "name": "PS4SX2", "title_ids": ["PCSX20007"], "detect": ["/data/PS4SX2"],
"note": "Two memory cards for every game, backed up to RomM (5.3 or newer). No save states yet.",
"profile": {
"root": "/data/PS4SX2", "rom_dir": "{root}/roms", "bios_dir": "{root}/bios",
"sync_saves": false, "memcards": "{root}/memcards/Mcd001.ps2,{root}/memcards/Mcd002.ps2",
"firmware": {"pick_one": ["21038400dc633070a78ad53090c53017", "dc69f0643a3030aaa4797501b483d6c4", "80ac46fa7e77b8ab4366e86948e54f83", "93ea3bcee4252627919175ff1b16a1d9", "d3e81e95db25f5a86a7b7474550a2155"]},
"platforms": [{"romm": ["ps2"], "dir": "ps2", "cards": {"default": "shared", "modes": "shared"}}]
}
},
{
"id": "nps64", "name": "NPS64", "title_ids": ["NPSX00064"], "detect": ["/data/NPS64"],
"note": "Saves and states are named after a hash of the game file (.z64 dumps).",
"profile": {
"root": "/data/NPS64", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/saves",
"bios_dir": "{root}/system", "fixed": ["rom_dir", "save_dir", "state_dir"],
"save_exts": ".srm", "save_name": "rom-{rom_fnv64}{ext}", "save_stems": "rom-{rom_fnv64}",
"state_exts": ".state", "state_stems": "rom-{rom_fnv64}", "state_name": "rom-{rom_fnv64}.slot1{ext}", "state_format": "raw",
"firmware": {"skip": true},
"platforms": [{"romm": ["n64"], "dir": "n64", "save_format": "n64-srm", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "mgbaps4", "name": "mGBA", "title_ids": ["MGBA00001"], "detect": ["/data/mgba"],
"profile": {
"root": "/data/mgba", "rom_dir": "{root}/roms", "save_dir": "{root}/saves", "state_dir": "{root}/states",
"save_exts": ".sav", "state_exts": ".ss*", "state_slot0": ".ss1", "state_format": "raw",
"platforms": [
{"romm": ["gba"], "dir": "gba"},
{"romm": ["gbc"], "dir": "gbc"},
{"romm": ["gb"], "dir": "gb"}
]
}
},
{
"id": "snesui", "name": "SNESUI", "title_ids": ["SNES00001", "SNES00009"], "detect": ["/data/snes-ps4"],
"profile": {
"root": "/data/snes-ps4", "rom_dir": "{root}/juegos", "save_dir": "{root}/guardados", "state_dir": "{root}/guardados",
"save_exts": ".srm", "state_exts": ".st0,.st1,.st2,.st3", "state_slot0": ".st0", "state_format": "raw",
"firmware": {"skip": true},
"platforms": [{"romm": ["snes", "sfam"], "dir": "snes"}]
}
[
{
"id": "psnes", "name": "pSNES", "title_ids": ["PSNE00001"], "detect": ["/data/psnes"],
"profile": {
"root": "/data/psnes", "rom_dir": "{root}/roms", "save_dir": "{root}/sram", "bios_dir": "{root}/bios",
"save_exts": ".srm",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"firmware": {"from": {"satellaview": ["BS-X.bin"], "sufami-turbo": ["STBIOS.bin"]}},
"platforms": [{"romm": ["snes", "sfam"], "dir": "snes"}]
}
]
}
},
{
"id": "pnes", "name": "pNES", "title_ids": ["PNES00001"], "detect": ["/data/pnes"],
"profile": {
"root": "/data/pnes", "rom_dir": "{root}/roms", "save_dir": "{root}/save", "bios_dir": "{root}/bios",
"save_exts": ".sav",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"firmware": {"dir": "{root}"},
"platforms": [{"romm": ["nes", "famicom"], "dir": "nes"}, {"romm": ["fds"], "dir": "fds"}]
}
},
{
"id": "pgen", "name": "pGEN", "title_ids": ["PGEN00001"], "detect": ["/data/pgen"],
"profile": {
"root": "/data/pgen", "rom_dir": "{root}/{platform}", "save_dir": "{root}/rams", "bios_dir": "{root}/bios",
"save_exts": ".srm",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"platforms": [
{"romm": ["genesis", "genesis-slash-megadrive", "megadrive"], "dir": "megadrive"},
{"romm": ["sms", "mastersystem"], "dir": "mastersystem"},
{"romm": ["gamegear", "gg"], "dir": "gamegear"},
{"romm": ["segacd", "sega-cd"], "dir": "megacd", "save_exts": ".brm"},
{"romm": ["sg1000"], "dir": "sg1000"}
]
}
},
{
"id": "pgba", "name": "pGBA", "title_ids": ["PGBA00001"], "detect": ["/data/pgba"],
"note": "Saves and states share the saves folder.",
"profile": {
"root": "/data/pgba", "rom_dir": "{root}/{platform}", "save_dir": "{root}/saves", "bios_dir": "{root}/bios",
"save_exts": ".sav",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"platforms": [
{"romm": ["gba"], "dir": "gba"},
{"romm": ["gbc"], "dir": "gbc"},
{"romm": ["gb"], "dir": "gb"}
]
}
},
{
"id": "pfbn", "name": "pFBN", "title_ids": ["PFBN00001"], "detect": ["/data/pfba"],
"note": "Arcade saves are the games' own memory (eeproms), backed up as they are.",
"profile": {
"root": "/data/pfba", "rom_dir": "{root}/{platform}", "save_dir": "{root}/eeproms", "bios_dir": "{root}/arcade",
"save_exts": ".fs",
"state_dir": "{root}/saves", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}-0,{rom_stem}-1,{rom_stem}-2,{rom_stem}-3", "state_name": "{rom_stem}-0{ext}",
"firmware": {"dir": "{rom_dir}"},
"platforms": [
{"romm": ["arcade", "fbneo", "neogeoaes", "neogeomvs", "cps1", "cps2", "cps3"], "dir": "arcade"},
{"romm": ["neo-geo-pocket-color", "neo-geo-pocket", "ngpc", "ngp"], "dir": "ngp"},
{"romm": ["colecovision"], "dir": "coleco"},
{"romm": ["msx"], "dir": "msx"},
{"romm": ["zxs", "zx-spectrum"], "dir": "spectrum"},
{"romm": ["supergrafx"], "dir": "sgx"},
{"romm": ["channel-f", "fairchild-channel-f"], "dir": "channelf"}
]
}
},
{
"id": "emusuni", "name": "EmuSuNi", "title_ids": ["EMSN00001"], "detect": ["/data/EmuSuNi"],
"note": "Set EmuSuNi's N64 folder to /data/EmuSuNi/n64 in its settings.",
"profile": {
"root": "/data/EmuSuNi", "bios_dir": "{root}/system",
"save_exts": ".srm",
"state_exts": ".state*", "state_format": "raw", "state_slot0": ".state1",
"platforms": [
{"romm": ["snes", "sfam"], "dir": "snes", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/states"},
{"romm": ["nes", "famicom"], "dir": "nes", "rom_dir": "{root}/nes", "save_dir": "{root}/saves/nes", "state_dir": "{root}/states/nes"},
{"romm": ["n64"], "dir": "n64", "rom_dir": "{root}/n64", "save_dir": "{root}/saves/n64", "state_dir": "{root}/states/n64", "save_format": "n64-srm", "cards": {"default": "per_game", "modes": "per_game"}}
]
}
},
{
"id": "dolphinps4", "name": "DolphinPS4", "title_ids": ["DLPH00010"], "detect": ["/data/DolphinPS4"],
"note": "Dolphin, with its user folder in {root}/User. States are named after the disc's id.",
"profile": {
"root": "/data/DolphinPS4", "rom_dir": "{root}/games", "save_dir": "{root}/User",
"state_dir": "{root}/User/StateSaves", "state_exts": ".s0*", "state_format": "raw",
"state_stems": "{game_id}", "state_name": "{game_id}{ext}",
"firmware": {"dir": "{root}/User/GC", "only_named": true, "names": {"b17148254a5799684c7d783206504926": "USA/IPL.bin", "fc924a7c879b661abc37cec4f018fdf3": "USA/IPL.bin", "019e39822a9ca3029124f74dd4d55ac4": "USA/IPL.bin", "db92574caab77a7ec99d4605fd6f2450": "EUR/IPL.bin", "339848a0b7c2124cf155276c1e79cbd0": "EUR/IPL.bin", "0cdda509e2da83c85bfe423dd87346cc": "EUR/IPL.bin"}},
"platforms": [
{"romm": ["ngc", "gamecube"], "dir": "gamecube", "emulator": "dolphin", "save_layout": "gci", "bundle_dirs": "{root}/User/GC", "cards": {"default": "per_game", "modes": "per_game"}},
{"romm": ["wii"], "dir": "wii", "emulator": "dolphin", "save_layout": "wii", "bundle_dirs": "{root}/User/Wii"}
]
}
},
{
"id": "resonance", "name": "Resonance", "title_ids": ["DCST00001"], "detect": ["/data/Resonance"],
"note": "Each game's own memory card syncs as its save; the shared ones are backed up as RomM memory cards.",
"profile": {
"root": "/data/Resonance", "rom_dir": "{root}/games", "save_dir": "{root}/data", "bios_dir": "{root}/data",
"save_exts": ".bin", "disc_id": "dreamcast",
"save_stems": "{game_id}_vmu_save_A1", "save_name": "{game_id}_vmu_save_A1{ext}",
"memcards": "{root}/data/vmu_save_A1.bin,{root}/data/vmu_save_B1.bin,{root}/data/vmu_save_C1.bin,{root}/data/vmu_save_D1.bin",
"state_dir": "{root}/data", "state_exts": ".state", "state_format": "raw",
"state_stems": "{rom_stem}_1,{rom_stem}_2,{rom_stem}_3", "state_name": "{rom_stem}{ext}",
"firmware": {"names": {"e10c53c2f8b90bab96ead2d368858623": "dc_boot.bin"}, "from": {"arcade": ["naomi.zip", "naomi2.zip", "awbios.zip"]}},
"platforms": [{"romm": ["dc", "dreamcast"], "dir": "dreamcast", "save_format": "dc-vmu", "cards": {"default": "per_game", "modes": "per_game,shared", "change": "Turn on Own memory card for each game in its settings (players 2 to 4 always share theirs)", "setting": {"file": "{root}/emu.cfg", "key": "PerGameVmu", "per_game": "yes,true", "shared": "no,false"}}}]
}
},
{
"id": "ps4sx2", "name": "PS4SX2", "title_ids": ["PCSX20007"], "detect": ["/data/PS4SX2"],
"note": "Two memory cards for every game, backed up to RomM (5.3 or newer). No save states yet.",
"profile": {
"root": "/data/PS4SX2", "rom_dir": "{root}/roms", "bios_dir": "{root}/bios",
"sync_saves": false, "memcards": "{root}/memcards/Mcd001.ps2,{root}/memcards/Mcd002.ps2",
"firmware": {"pick_one": ["21038400dc633070a78ad53090c53017", "dc69f0643a3030aaa4797501b483d6c4", "80ac46fa7e77b8ab4366e86948e54f83", "93ea3bcee4252627919175ff1b16a1d9", "d3e81e95db25f5a86a7b7474550a2155"]},
"platforms": [{"romm": ["ps2"], "dir": "ps2", "cards": {"default": "shared", "modes": "shared"}}]
}
},
{
"id": "nps64", "name": "NPS64", "title_ids": ["NPSX00064"], "detect": ["/data/NPS64"],
"note": "Saves and states are named after a hash of the game file (.z64 dumps).",
"profile": {
"root": "/data/NPS64", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/saves",
"bios_dir": "{root}/system", "fixed": ["rom_dir", "save_dir", "state_dir"],
"save_exts": ".srm", "save_name": "rom-{rom_fnv64}{ext}", "save_stems": "rom-{rom_fnv64}",
"state_exts": ".state", "state_stems": "rom-{rom_fnv64}", "state_name": "rom-{rom_fnv64}.slot1{ext}", "state_format": "raw",
"firmware": {"skip": true},
"platforms": [{"romm": ["n64"], "dir": "n64", "save_format": "n64-srm", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "mgbaps4", "name": "mGBA", "title_ids": ["MGBA00001"], "detect": ["/data/mgba"],
"profile": {
"root": "/data/mgba", "rom_dir": "{root}/roms", "save_dir": "{root}/saves", "state_dir": "{root}/states",
"save_exts": ".sav", "state_exts": ".ss*", "state_slot0": ".ss1", "state_format": "raw",
"platforms": [
{"romm": ["gba"], "dir": "gba"},
{"romm": ["gbc"], "dir": "gbc"},
{"romm": ["gb"], "dir": "gb"}
]
}
},
{
"id": "snesui", "name": "SNESUI", "title_ids": ["SNES00001", "SNES00009"], "detect": ["/data/snes-ps4"],
"profile": {
"root": "/data/snes-ps4", "rom_dir": "{root}/juegos", "save_dir": "{root}/guardados", "state_dir": "{root}/guardados",
"save_exts": ".srm", "state_exts": ".st0,.st1,.st2,.st3", "state_slot0": ".st0", "state_format": "raw",
"firmware": {"skip": true},
"platforms": [{"romm": ["snes", "sfam"], "dir": "snes"}]
}
}
]
@@ -735,11 +735,6 @@ void App::refresh_status()
repaired_ = true;
repair_autostart();
}
if (!presets_checked_ && !payload_too_old()) {
presets_checked_ = true;
api_.post("/api/presets/check", "{}", [](const Response &) {});
}
status_.ip = str(j.get(), "ip");
status_.port = static_cast<int>(num(j.get(), "web_port", 8780));
status_.paired = flag(j.get(), "paired");
@@ -405,7 +405,7 @@ class App
bool bundled_read_ = false;
std::atomic<int> start_state_{0}; // 0 idle, 1 sending, 2 sent, 3 failed
std::string start_error_; // written before start_state_ becomes 3
bool tried_start_ = false, updated_payload_ = false, repaired_ = false, presets_checked_ = false;
bool tried_start_ = false, updated_payload_ = false, repaired_ = false;
bool payload_waiting_ = false;
int unanswered_ = 0; // status requests in a row RomM Sync didn't answer
int active_queue_ = 0;
+193 -199
View File
@@ -1,201 +1,195 @@
{
"format": 1,
"version": "2026.10.23.1",
"console": "ps5",
"emulators": [
[
{
"id": "psxs5", "name": "PSXS5",
"title_ids": ["PPSA97510"],
"detect": ["/data/PSXS5", "/data/homebrew/PPSA97510"],
"note": "One memory card per game, named after the game file (after the disc with PCSX-ReARMed).",
"profile": {
"root": "/data/PSXS5", "mount_games": "data", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/states",
"bios_dir": "{root}/bios",
"save_exts": ".mcd", "save_name": "{serial}_1{ext}", "save_stems": "{serial}_1",
"state_exts": ".state*", "state_stems": "{rom_stem_safe}", "state_name": "{rom_stem_safe}{ext}",
"save_name_by_core": {"mednafen_psx_hw": "{rom_stem}_1{ext}"},
"state_format": "raw", "state_slot0": ".state0", "state_cores": "pcsx_rearmed,mednafen_psx_hw",
"state_core_setting": {"file": "{root}/psxs5.ini", "key": "emulator",
"values": {"0": "mednafen_psx_hw", "1": "pcsx_rearmed", "2": "mednafen_psx_hw"},
"auto": "0", "log": "{root}/logs/psxs5.log", "log_line": "host: emulator ",
"log_names": {"rearmed": "pcsx_rearmed", "beetle": "mednafen_psx_hw", "mednafen": "mednafen_psx_hw"}},
"firmware": {"names": {"8dd7d5296a650fac7319bce665a6a53c": "scph5500.bin", "490f666e1afb15b7362b406ed1cea246": "scph5501.bin",
"32736f17079d0b2b7024407c39bd3050": "scph5502.bin"}},
"platforms": [{"romm": ["psx", "ps1", "playstation"], "dir": "psx", "save_format": "psx-card", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "swanstationps5", "name": "SwanStationPS5",
"title_ids": ["PPSA98510"],
"detect": ["/data/SwanStationPS5", "/data/homebrew/PPSA98510"],
"note": "Plays the games in PSXS5's folder. One memory card per game, named after the game file. Its save states load only in SwanStationPS5.",
"profile": {
"root": "/data/SwanStationPS5", "mount_games": "data", "rom_dir": "/data/PSXS5/games", "save_dir": "{root}/saves", "state_dir": "{root}/states",
"bios_dir": "{root}/bios",
"save_exts": ".mcd", "save_name": "{rom_stem}_1{ext}", "save_stems": "{rom_stem}_1",
"state_exts": ".state*", "state_stems": "{rom_stem_safe}", "state_name": "{rom_stem_safe}{ext}",
"state_format": "raw", "state_slot0": ".state0", "state_cores": "swanstation",
"platforms": [{"romm": ["psx", "ps1", "playstation"], "dir": "psx", "save_format": "psx-card", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "psswanstation", "name": "PSSwanStation",
"title_ids": ["PPSA99248"],
"detect": ["/data/homebrew/PPSA99248"],
"note": "One memory card per game, named after the game file. Its save states load only in PSSwanStation.",
"profile": {
"root": "/data/homebrew/PPSA99248", "rom_dir": "{root}/games", "save_dir": "{root}/data/saves",
"state_dir": "{root}/data/states", "bios_dir": "{root}/bios",
"mount_games": true,
"save_exts": ".mcd", "save_name": "{rom_stem}_1{ext}", "save_stems": "{rom_stem}_1",
"state_exts": ".state", "state_name": "{rom_stem}{ext}", "state_slot0": ".0.state",
"state_format": "raw", "state_cores": "psswanstation",
"platforms": [{"romm": ["psx", "ps1", "playstation"], "dir": "psx", "save_format": "psx-card", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "ps64", "name": "PS64",
"title_ids": ["PPSA06464"],
"detect": ["/data/ps64", "/data/homebrew/PPSA06464"],
"note": "Games save into their own disk images. Save Disks sync; there are no save states.",
"profile": {
"root": "/data/ps64", "rom_dir": "{root}/games", "save_dir": "{root}/emulator/saves",
"state_dir": "{root}/emulator/saves", "bios_dir": "{root}/emulator/system",
"save_exts": ".d64", "save_name": "{rom_stem}.save0{ext}", "state_exts": ".vsf", "state_cores": "ps64",
"firmware": {"skip": true},
"platforms": [{"romm": ["c64"], "dir": "c64", "emulator": "vice"}]
}
},
{
"id": "ps5sx2", "name": "PS5SX2",
"title_ids": ["PPSA99203"],
"detect": ["/data/PCSX2", "/data/homebrew/PPSA99203"],
"note": "Two memory cards for every game, backed up to RomM (5.3 or newer).",
"profile": {
"root": "/data/PCSX2", "rom_dir": "{root}/games", "bios_dir": "{root}",
"sync_saves": false, "memcards": "{root}/Mcd001.ps2,{root}/Mcd002.ps2",
"firmware": {"pick_one": ["21038400dc633070a78ad53090c53017", "dc69f0643a3030aaa4797501b483d6c4", "80ac46fa7e77b8ab4366e86948e54f83",
"93ea3bcee4252627919175ff1b16a1d9", "d3e81e95db25f5a86a7b7474550a2155"]},
"platforms": [{"romm": ["ps2"], "dir": "ps2", "cards": {"default": "shared", "modes": "shared"}}]
}
},
{
"id": "ps5n64", "name": "PS5N64",
"title_ids": ["PPSA99206"],
"detect": ["/data/PS5N64", "/data/homebrew/PPSA99206"],
"note": "Saves and states are named after a hash of the game file.",
"profile": {
"root": "/data/PS5N64", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/saves",
"fixed": ["rom_dir", "save_dir", "state_dir"],
"save_exts": ".sav", "save_name": "{rom_fnv64}{ext}", "save_stems": "{rom_fnv64}",
"state_exts": ".state", "state_stems": "{rom_fnv64}", "state_name": "{rom_fnv64}{ext}", "state_format": "raw",
"firmware": {"skip": true},
"platforms": [{"romm": ["n64"], "dir": "n64", "save_format": "n64-srm", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "ps5nes", "name": "PS5NES",
"title_ids": ["PPSA99204"],
"detect": ["/data/PS5NES", "/data/homebrew/PPSA99204"],
"note": "Untested: v7 may look for games in PS5SX2's folder.",
"profile": {
"root": "/data/PS5NES", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/saves",
"save_exts": ".sav", "save_name": "{rom_fnv64}{ext}", "save_stems": "{rom_fnv64}",
"firmware": {"skip": true},
"platforms": [{"romm": ["nes", "famicom"], "dir": "nes"}]
}
},
{
"id": "porpoise", "name": "Porpoise",
"title_ids": ["PPSA99764"],
"detect": ["/data/porpoise", "/data/homebrew/PPSA99764"],
"note": "Dolphin, with its user folder in {root}/saves/User.",
"profile": {
"root": "/data/porpoise", "rom_dir": "{root}/games", "save_dir": "{root}/saves/User",
"state_dir": "{root}/states/{game_id}", "state_exts": ".state", "state_format": "raw",
"bios_dir": "{root}/bios",
"firmware": {"only_named": true, "names": {
"b17148254a5799684c7d783206504926": "gc-ntsc-ipl.bin", "fc924a7c879b661abc37cec4f018fdf3": "gc-ntsc-ipl.bin",
"019e39822a9ca3029124f74dd4d55ac4": "gc-ntsc-ipl.bin", "db92574caab77a7ec99d4605fd6f2450": "gc-pal-ipl.bin",
"339848a0b7c2124cf155276c1e79cbd0": "gc-pal-ipl.bin", "0cdda509e2da83c85bfe423dd87346cc": "gc-pal-ipl.bin"}},
"platforms": [
{"romm": ["ngc", "gamecube"], "dir": "gamecube", "emulator": "dolphin", "save_layout": "gci", "bundle_dirs": "{root}/saves/User/GC", "cards": {"default": "per_game", "modes": "per_game"}},
{"romm": ["wii"], "dir": "wii", "emulator": "dolphin", "save_layout": "wii", "bundle_dirs": "{root}/saves/User/Wii"}
]
}
},
{
"id": "psflycast", "name": "PSFlyCast",
"title_ids": ["PPSA99247"],
"detect": ["/data/homebrew/PPSA99247"],
"note": "Each game's own memory card syncs as its save; the shared ones (Own memory card for each game off) are backed up as RomM memory cards. Save states don't sync yet.",
"profile": {
"root": "/data/homebrew/PPSA99247", "rom_dir": "{root}/games", "save_dir": "{root}/data",
"state_dir": "{root}/data/savestates", "bios_dir": "{root}/bios",
"save_exts": ".bin", "state_exts": "", "game_ids": "{root}/data/game-ids.txt", "disc_id": "dreamcast",
"save_stems": "{game_id}_vmu_save_A1", "save_name": "{game_id}_vmu_save_A1{ext}",
"memcards": "{root}/data/vmu_save_A1.bin,{root}/data/vmu_save_B1.bin,{root}/data/vmu_save_C1.bin,{root}/data/vmu_save_D1.bin",
"firmware": {"names": {"e10c53c2f8b90bab96ead2d368858623": "dc_boot.bin", "0a93f7940c455905bea6e392dfde92a4": "dc_flash.bin"},
"from": {"arcade": ["naomi.zip", "naomi2.zip", "awbios.zip"]}},
"platforms": [{"romm": ["dc", "dreamcast"], "dir": "dc", "emulator": "flycast", "save_format": "dc-vmu", "cards": {"default": "per_game", "modes": "per_game,shared", "change": "Turn on Own memory card for each game in its settings (players 2 to 4 always share theirs)", "setting": {"file": "{root}/emu.cfg", "key": "PerGameVmu", "per_game": "yes,true", "shared": "no,false"}}}]
}
},
{
"id": "seedee5", "name": "Seedee5",
"title_ids": ["PPSA99142"],
"detect": ["/data/homebrew/PPSA99142"],
"note": "Games and BIOS files only: saved games are kept as folders, and only after a clean exit.",
"profile": {
"root": "/data/homebrew/PPSA99142/data", "rom_dir": "{root}/roms", "save_dir": "{root}/saves",
"bios_dir": "{root}/system/same_cdi/bios", "sync_saves": false,
"platforms": [{"romm": ["philips-cd-i"], "dir": "cdi", "emulator": "same_cdi"}]
}
},
{
"id": "xpsemu", "name": "XPSemu",
"title_ids": ["PPSA97358"],
"detect": ["/data/xemu", "/data/homebrew/PPSA97358"],
"note": "Games and BIOS files only: saves live inside its hard disk image.",
"profile": {
"root": "/data/xemu", "rom_dir": "{root}/games", "bios_dir": "{root}", "sync_saves": false,
"firmware": {"only_files": ["mcpx_1.0.bin", "Complex_4627.bin"]},
"platforms": [{"romm": ["xbox"], "dir": "xbox", "emulator": "xemu"}]
}
},
{
"id": "ps5cemu", "name": "PS5CEMU-HAR",
"title_ids": ["PPSA99360"],
"detect": ["/data/ps5cemu", "/data/homebrew/PPSA99360"],
"note": "Wii U (Cemu), 3DS (Azahar) and DS (melonDS). Saves sync for all three; 3DS and DS save states load only in PS5CEMU-HAR.",
"profile": {
"root": "/data/ps5cemu",
"platforms": [
{"romm": ["wiiu"], "dir": "wiiu", "emulator": "cemu", "rom_dir": "{root}/games", "bios_dir": "{root}",
"save_layout": "wiiu", "bundle_dirs": "{root}/mlc01/usr/save",
"firmware": {"only_files": ["keys.txt"]}},
{"romm": ["3ds"], "dir": "3ds", "emulator": "azahar", "rom_dir": "{root}/azahar/games", "bios_dir": "{root}/azahar/sysdata",
"save_layout": "3ds", "bundle_dirs": "{root}/azahar/sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/title",
"state_dir": "{root}/azahar/states", "state_exts": ".cst", "state_stems": "{game_id}", "state_name": "{game_id}.01{ext}",
"state_format": "raw", "state_cores": "ps5cemu-azahar",
"firmware": {"only_files": ["aes_keys.txt", "seeddb.bin"]}},
{"romm": ["nds"], "dir": "nds", "emulator": "melonds", "rom_dir": "{root}/melonds/games",
"save_dir": "{root}/melonds/saves", "save_exts": ".sav", "bios_dir": "{root}/melonds/bios", "sync_saves": true,
"state_dir": "{root}/melonds/states", "state_exts": ".ml*", "state_format": "raw", "state_cores": "ps5cemu-melonds",
"firmware": {"only_files": ["bios7.bin", "bios9.bin", "firmware.bin"]}}
]
}
},
{
"id": "prosperoeden", "name": "ProsperoEden",
"title_ids": ["PPSA99008", "PPSA99186"],
"detect": ["/data/prosperoeden", "/data/homebrew/PPSA99008", "/data/homebrew/PPSA99186"],
"note": "Games (with their updates and DLC) and keys only: saves are kept per Switch user profile. Its Switch UI app shares the same folders.",
"profile": {
"root": "/data/prosperoeden", "rom_dir": "{root}/roms", "update_dir": "{root}/updates", "dlc_dir": "{root}/updates",
"bios_dir": "{root}", "sync_saves": false,
"firmware": {"by_ext": {".keys": "keys", ".nca": "firmware"}},
"platforms": [{"romm": ["switch"], "dir": "switch", "emulator": "prosperoeden"}]
}
"id": "psxs5", "name": "PSXS5",
"title_ids": ["PPSA97510"],
"detect": ["/data/PSXS5", "/data/homebrew/PPSA97510"],
"note": "One memory card per game, named after the game file (after the disc with PCSX-ReARMed).",
"profile": {
"root": "/data/PSXS5", "mount_games": "data", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/states",
"bios_dir": "{root}/bios",
"save_exts": ".mcd", "save_name": "{serial}_1{ext}", "save_stems": "{serial}_1",
"state_exts": ".state*", "state_stems": "{rom_stem_safe}", "state_name": "{rom_stem_safe}{ext}",
"save_name_by_core": {"mednafen_psx_hw": "{rom_stem}_1{ext}"},
"state_format": "raw", "state_slot0": ".state0", "state_cores": "pcsx_rearmed,mednafen_psx_hw",
"state_core_setting": {"file": "{root}/psxs5.ini", "key": "emulator",
"values": {"0": "mednafen_psx_hw", "1": "pcsx_rearmed", "2": "mednafen_psx_hw"},
"auto": "0", "log": "{root}/logs/psxs5.log", "log_line": "host: emulator ",
"log_names": {"rearmed": "pcsx_rearmed", "beetle": "mednafen_psx_hw", "mednafen": "mednafen_psx_hw"}},
"firmware": {"names": {"8dd7d5296a650fac7319bce665a6a53c": "scph5500.bin", "490f666e1afb15b7362b406ed1cea246": "scph5501.bin",
"32736f17079d0b2b7024407c39bd3050": "scph5502.bin"}},
"platforms": [{"romm": ["psx", "ps1", "playstation"], "dir": "psx", "save_format": "psx-card", "cards": {"default": "per_game", "modes": "per_game"}}]
}
]
}
},
{
"id": "swanstationps5", "name": "SwanStationPS5",
"title_ids": ["PPSA98510"],
"detect": ["/data/SwanStationPS5", "/data/homebrew/PPSA98510"],
"note": "Plays the games in PSXS5's folder. One memory card per game, named after the game file. Its save states load only in SwanStationPS5.",
"profile": {
"root": "/data/SwanStationPS5", "mount_games": "data", "rom_dir": "/data/PSXS5/games", "save_dir": "{root}/saves", "state_dir": "{root}/states",
"bios_dir": "{root}/bios",
"save_exts": ".mcd", "save_name": "{rom_stem}_1{ext}", "save_stems": "{rom_stem}_1",
"state_exts": ".state*", "state_stems": "{rom_stem_safe}", "state_name": "{rom_stem_safe}{ext}",
"state_format": "raw", "state_slot0": ".state0", "state_cores": "swanstation",
"platforms": [{"romm": ["psx", "ps1", "playstation"], "dir": "psx", "save_format": "psx-card", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "psswanstation", "name": "PSSwanStation",
"title_ids": ["PPSA99248"],
"detect": ["/data/homebrew/PPSA99248"],
"note": "One memory card per game, named after the game file. Its save states load only in PSSwanStation.",
"profile": {
"root": "/data/homebrew/PPSA99248", "rom_dir": "{root}/games", "save_dir": "{root}/data/saves",
"state_dir": "{root}/data/states", "bios_dir": "{root}/bios",
"mount_games": true,
"save_exts": ".mcd", "save_name": "{rom_stem}_1{ext}", "save_stems": "{rom_stem}_1",
"state_exts": ".state", "state_name": "{rom_stem}{ext}", "state_slot0": ".0.state",
"state_format": "raw", "state_cores": "psswanstation",
"platforms": [{"romm": ["psx", "ps1", "playstation"], "dir": "psx", "save_format": "psx-card", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "ps64", "name": "PS64",
"title_ids": ["PPSA06464"],
"detect": ["/data/ps64", "/data/homebrew/PPSA06464"],
"note": "Games save into their own disk images. Save Disks sync; there are no save states.",
"profile": {
"root": "/data/ps64", "rom_dir": "{root}/games", "save_dir": "{root}/emulator/saves",
"state_dir": "{root}/emulator/saves", "bios_dir": "{root}/emulator/system",
"save_exts": ".d64", "save_name": "{rom_stem}.save0{ext}", "state_exts": ".vsf", "state_cores": "ps64",
"firmware": {"skip": true},
"platforms": [{"romm": ["c64"], "dir": "c64", "emulator": "vice"}]
}
},
{
"id": "ps5sx2", "name": "PS5SX2",
"title_ids": ["PPSA99203"],
"detect": ["/data/PCSX2", "/data/homebrew/PPSA99203"],
"note": "Two memory cards for every game, backed up to RomM (5.3 or newer).",
"profile": {
"root": "/data/PCSX2", "rom_dir": "{root}/games", "bios_dir": "{root}",
"sync_saves": false, "memcards": "{root}/Mcd001.ps2,{root}/Mcd002.ps2",
"firmware": {"pick_one": ["21038400dc633070a78ad53090c53017", "dc69f0643a3030aaa4797501b483d6c4", "80ac46fa7e77b8ab4366e86948e54f83",
"93ea3bcee4252627919175ff1b16a1d9", "d3e81e95db25f5a86a7b7474550a2155"]},
"platforms": [{"romm": ["ps2"], "dir": "ps2", "cards": {"default": "shared", "modes": "shared"}}]
}
},
{
"id": "ps5n64", "name": "PS5N64",
"title_ids": ["PPSA99206"],
"detect": ["/data/PS5N64", "/data/homebrew/PPSA99206"],
"note": "Saves and states are named after a hash of the game file.",
"profile": {
"root": "/data/PS5N64", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/saves",
"fixed": ["rom_dir", "save_dir", "state_dir"],
"save_exts": ".sav", "save_name": "{rom_fnv64}{ext}", "save_stems": "{rom_fnv64}",
"state_exts": ".state", "state_stems": "{rom_fnv64}", "state_name": "{rom_fnv64}{ext}", "state_format": "raw",
"firmware": {"skip": true},
"platforms": [{"romm": ["n64"], "dir": "n64", "save_format": "n64-srm", "cards": {"default": "per_game", "modes": "per_game"}}]
}
},
{
"id": "ps5nes", "name": "PS5NES",
"title_ids": ["PPSA99204"],
"detect": ["/data/PS5NES", "/data/homebrew/PPSA99204"],
"note": "Untested: v7 may look for games in PS5SX2's folder.",
"profile": {
"root": "/data/PS5NES", "rom_dir": "{root}/games", "save_dir": "{root}/saves", "state_dir": "{root}/saves",
"save_exts": ".sav", "save_name": "{rom_fnv64}{ext}", "save_stems": "{rom_fnv64}",
"firmware": {"skip": true},
"platforms": [{"romm": ["nes", "famicom"], "dir": "nes"}]
}
},
{
"id": "porpoise", "name": "Porpoise",
"title_ids": ["PPSA99764"],
"detect": ["/data/porpoise", "/data/homebrew/PPSA99764"],
"note": "Dolphin, with its user folder in {root}/saves/User.",
"profile": {
"root": "/data/porpoise", "rom_dir": "{root}/games", "save_dir": "{root}/saves/User",
"state_dir": "{root}/states/{game_id}", "state_exts": ".state", "state_format": "raw",
"bios_dir": "{root}/bios",
"firmware": {"only_named": true, "names": {
"b17148254a5799684c7d783206504926": "gc-ntsc-ipl.bin", "fc924a7c879b661abc37cec4f018fdf3": "gc-ntsc-ipl.bin",
"019e39822a9ca3029124f74dd4d55ac4": "gc-ntsc-ipl.bin", "db92574caab77a7ec99d4605fd6f2450": "gc-pal-ipl.bin",
"339848a0b7c2124cf155276c1e79cbd0": "gc-pal-ipl.bin", "0cdda509e2da83c85bfe423dd87346cc": "gc-pal-ipl.bin"}},
"platforms": [
{"romm": ["ngc", "gamecube"], "dir": "gamecube", "emulator": "dolphin", "save_layout": "gci", "bundle_dirs": "{root}/saves/User/GC", "cards": {"default": "per_game", "modes": "per_game"}},
{"romm": ["wii"], "dir": "wii", "emulator": "dolphin", "save_layout": "wii", "bundle_dirs": "{root}/saves/User/Wii"}
]
}
},
{
"id": "psflycast", "name": "PSFlyCast",
"title_ids": ["PPSA99247"],
"detect": ["/data/homebrew/PPSA99247"],
"note": "Each game's own memory card syncs as its save; the shared ones (Own memory card for each game off) are backed up as RomM memory cards. Save states don't sync yet.",
"profile": {
"root": "/data/homebrew/PPSA99247", "rom_dir": "{root}/games", "save_dir": "{root}/data",
"state_dir": "{root}/data/savestates", "bios_dir": "{root}/bios",
"save_exts": ".bin", "state_exts": "", "game_ids": "{root}/data/game-ids.txt", "disc_id": "dreamcast",
"save_stems": "{game_id}_vmu_save_A1", "save_name": "{game_id}_vmu_save_A1{ext}",
"memcards": "{root}/data/vmu_save_A1.bin,{root}/data/vmu_save_B1.bin,{root}/data/vmu_save_C1.bin,{root}/data/vmu_save_D1.bin",
"firmware": {"names": {"e10c53c2f8b90bab96ead2d368858623": "dc_boot.bin", "0a93f7940c455905bea6e392dfde92a4": "dc_flash.bin"},
"from": {"arcade": ["naomi.zip", "naomi2.zip", "awbios.zip"]}},
"platforms": [{"romm": ["dc", "dreamcast"], "dir": "dc", "emulator": "flycast", "save_format": "dc-vmu", "cards": {"default": "per_game", "modes": "per_game,shared", "change": "Turn on Own memory card for each game in its settings (players 2 to 4 always share theirs)", "setting": {"file": "{root}/emu.cfg", "key": "PerGameVmu", "per_game": "yes,true", "shared": "no,false"}}}]
}
},
{
"id": "seedee5", "name": "Seedee5",
"title_ids": ["PPSA99142"],
"detect": ["/data/homebrew/PPSA99142"],
"note": "Games and BIOS files only: saved games are kept as folders, and only after a clean exit.",
"profile": {
"root": "/data/homebrew/PPSA99142/data", "rom_dir": "{root}/roms", "save_dir": "{root}/saves",
"bios_dir": "{root}/system/same_cdi/bios", "sync_saves": false,
"platforms": [{"romm": ["philips-cd-i"], "dir": "cdi", "emulator": "same_cdi"}]
}
},
{
"id": "xpsemu", "name": "XPSemu",
"title_ids": ["PPSA97358"],
"detect": ["/data/xemu", "/data/homebrew/PPSA97358"],
"note": "Games and BIOS files only: saves live inside its hard disk image.",
"profile": {
"root": "/data/xemu", "rom_dir": "{root}/games", "bios_dir": "{root}", "sync_saves": false,
"firmware": {"only_files": ["mcpx_1.0.bin", "Complex_4627.bin"]},
"platforms": [{"romm": ["xbox"], "dir": "xbox", "emulator": "xemu"}]
}
},
{
"id": "ps5cemu", "name": "PS5CEMU-HAR",
"title_ids": ["PPSA99360"],
"detect": ["/data/ps5cemu", "/data/homebrew/PPSA99360"],
"note": "Wii U (Cemu), 3DS (Azahar) and DS (melonDS). Wii U and 3DS saves sync; 3DS save states load only in PS5CEMU-HAR. DS: games and BIOS files only for now.",
"profile": {
"root": "/data/ps5cemu",
"platforms": [
{"romm": ["wiiu"], "dir": "wiiu", "emulator": "cemu", "rom_dir": "{root}/games", "bios_dir": "{root}",
"save_layout": "wiiu", "bundle_dirs": "{root}/mlc01/usr/save",
"firmware": {"only_files": ["keys.txt"]}},
{"romm": ["3ds"], "dir": "3ds", "emulator": "azahar", "rom_dir": "{root}/azahar/games", "bios_dir": "{root}/azahar/sysdata",
"save_layout": "3ds", "bundle_dirs": "{root}/azahar/sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000/title",
"state_dir": "{root}/azahar/states", "state_exts": ".cst", "state_stems": "{game_id}", "state_name": "{game_id}.01{ext}",
"state_format": "raw", "state_cores": "ps5cemu-azahar",
"firmware": {"only_files": ["aes_keys.txt", "seeddb.bin"]}},
{"romm": ["nds"], "dir": "nds", "emulator": "melonds", "rom_dir": "{root}/melonds/games",
"save_dir": "{root}/melonds/saves", "bios_dir": "{root}/melonds/bios", "sync_saves": false,
"firmware": {"only_files": ["bios7.bin", "bios9.bin", "firmware.bin"]}}
]
}
},
{
"id": "prosperoeden", "name": "ProsperoEden",
"title_ids": ["PPSA99008", "PPSA99186"],
"detect": ["/data/prosperoeden", "/data/homebrew/PPSA99008", "/data/homebrew/PPSA99186"],
"note": "Games (with their updates and DLC) and keys only: saves are kept per Switch user profile. Its Switch UI app shares the same folders.",
"profile": {
"root": "/data/prosperoeden", "rom_dir": "{root}/roms", "update_dir": "{root}/updates", "dlc_dir": "{root}/updates",
"bios_dir": "{root}", "sync_saves": false,
"firmware": {"by_ext": {".keys": "keys", ".nca": "firmware"}},
"platforms": [{"romm": ["switch"], "dir": "switch", "emulator": "prosperoeden"}]
}
}
]
+8 -73
View File
@@ -23,7 +23,6 @@
#include "ra2.h"
#include "state.h"
#include "util.h"
#include "presets.h"
/* Bump this when generated profiles gain new keys, or existing installs keep
* their old profile forever (ask me how I know). */
@@ -383,14 +382,6 @@ static int app_installed(const char *title_id) {
return 0;
}
/* Presets typically are {"format", "version", "console" "emulators": [....]}*/
static cJSON *catalog_from(cJSON *file) {
if (cJSON_IsArray(file)) return file;
cJSON *list = cJSON_DetachItemFromObjectCaseSensitive(file, "emulators");
cJSON_Delete(file);
return list;
}
/* The catalog: ROMM_SYNC_CATALOG names a file (for tests), else the console's. */
static cJSON *emulator_catalog(void) {
const char *env = getenv("ROMM_SYNC_CATALOG");
@@ -398,9 +389,9 @@ static cJSON *emulator_catalog(void) {
char *txt = read_file(env, NULL);
cJSON *j = txt ? cJSON_Parse(txt) : NULL;
free(txt);
return catalog_from(j);
return j;
}
return cJSON_Duplicate(presets_catalog(), 1);
return plat_info()->emulator_catalog ? cJSON_Parse(plat_info()->emulator_catalog) : NULL;
}
static cJSON *g_catalog;
@@ -408,13 +399,9 @@ static void load_catalog(void) { g_catalog = emulator_catalog(); }
const cJSON *detect_catalog_entry(const char *id) {
static pthread_once_t once = PTHREAD_ONCE_INIT;
const char *env = getenv("ROMM_SYNC_CATALOG");
const cJSON *catalog = presets_catalog(), *e;
if (env && *env) {
pthread_once(&once, load_catalog);
catalog = g_catalog;
}
cJSON_ArrayForEach(e, catalog)
pthread_once(&once, load_catalog);
const cJSON *e;
cJSON_ArrayForEach(e, g_catalog)
if (!strcmp(jget_str(e, "id", ""), id)) return e;
return NULL;
}
@@ -537,57 +524,6 @@ cJSON *detect_emulators(void) {
* preset, which follow it (PSXS5's cores, as its versions change them). */
static const char *const PRESET_ONLY[] = {"state_core_setting"};
/* "sync_saves" says whether an emulator's saves can sync, and only the preset
* gets to say it now (profiles.c). Saved profiles still carry copies from
* before: a "false" the preset never shipped was the user switching sync off,
* which "sync": "off" says now; one it did ship (and has since dropped, the
* saves turning out fine) is simply gone. Every "false" ever shipped, by
* emulator and system ("*": all of them): */
static const struct {
const char *profile, *system;
} SHIPPED_NO_SYNC[] = {
{"ps5cemu", "nds"}, {"prosperoeden", "*"}, {"ps4sx2", "*"}, {"ps5sx2", "*"},
{"resonance", "*"}, {"seedee5", "*"}, {"xpsemu", "*"},
};
static int shipped_no_sync(const char *profile, const char *system) {
for (size_t i = 0; i < sizeof SHIPPED_NO_SYNC / sizeof SHIPPED_NO_SYNC[0]; i++)
if (!strcmp(SHIPPED_NO_SYNC[i].profile, profile) &&
(!strcmp(SHIPPED_NO_SYNC[i].system, "*") || !strcmp(SHIPPED_NO_SYNC[i].system, system)))
return 1;
return 0;
}
/* One level's saved copy (the profile's, system "*", or a platform's) gone;
* the user's own "off" kept as "sync". `preset` is what the preset says for
* that level now: a copy of it isn't anyone's choice either. */
static int drop_saved_sync_saves(cJSON *o, const cJSON *preset, const char *profile, const char *system, const char *name) {
const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, "sync_saves");
if (!v) return 0;
if (cJSON_IsFalse(v) && !cJSON_IsFalse(preset) && !shipped_no_sync(profile, system)) {
cJSON_DeleteItemFromObjectCaseSensitive(o, "sync");
cJSON_AddStringToObject(o, "sync", "off");
LOGI("the %s profile: sync turned off for %s, as its saved profile had it", name, !strcmp(system, "*") ? "all its systems" : system);
}
cJSON_DeleteItemFromObjectCaseSensitive(o, "sync_saves");
return 1;
}
static int drop_saved_sync(cJSON *p, const cJSON *preset) {
const char *id = jget_str(p, "id", ""), *name = jget_str(p, "name", "");
const cJSON *top = cJSON_GetObjectItemCaseSensitive(preset, "sync_saves");
int n = drop_saved_sync_saves(p, top, id, "*", name);
cJSON *pe;
cJSON_ArrayForEach(pe, cJSON_GetObjectItemCaseSensitive(p, "platforms")) {
const cJSON *ce, *says = top;
cJSON_ArrayForEach(ce, cJSON_GetObjectItemCaseSensitive(preset, "platforms"))
if (!strcmp(profiles_system_key(ce), profiles_system_key(pe)) && cJSON_GetObjectItemCaseSensitive(ce, "sync_saves"))
says = cJSON_GetObjectItemCaseSensitive(ce, "sync_saves");
n += drop_saved_sync_saves(pe, says, id, profiles_system_key(pe), name);
}
return n;
}
static int fill_from_catalog(cJSON *p) {
const cJSON *entry = detect_catalog_entry(jget_str(p, "id", ""));
const cJSON *preset = entry ? cJSON_GetObjectItemCaseSensitive(entry, "profile") : NULL, *k;
@@ -605,7 +541,7 @@ static int fill_from_catalog(cJSON *p) {
return added;
}
cJSON_ArrayForEach(k, preset) {
if (!strcmp(k->string, "platforms") || !strcmp(k->string, "sync_saves")) continue; /* (the preset's alone) */
if (!strcmp(k->string, "platforms")) continue;
const cJSON *have = cJSON_GetObjectItemCaseSensitive(p, k->string);
int preset_only = 0;
for (size_t i = 0; i < sizeof PRESET_ONLY / sizeof PRESET_ONLY[0]; i++) preset_only |= !strcmp(k->string, PRESET_ONLY[i]);
@@ -617,7 +553,6 @@ static int fill_from_catalog(cJSON *p) {
added++;
}
}
const int dropped = drop_saved_sync(p, preset);
cJSON_ArrayForEach(pe, cJSON_GetObjectItemCaseSensitive(p, "platforms")) {
const cJSON *first = cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(pe, "romm"), 0), *ce;
if (!cJSON_IsString(first)) continue;
@@ -625,14 +560,14 @@ static int fill_from_catalog(cJSON *p) {
const cJSON *cf = cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(ce, "romm"), 0);
if (!cJSON_IsString(cf) || strcmp(cf->valuestring, first->valuestring)) continue;
cJSON_ArrayForEach(k, ce)
if (strcmp(k->string, "sync_saves") != 0 && !cJSON_GetObjectItemCaseSensitive(pe, k->string)) {
if (!cJSON_GetObjectItemCaseSensitive(pe, k->string)) {
cJSON_AddItemToObject(pe, k->string, cJSON_Duplicate(k, 1));
added++;
}
}
}
if (added) LOGI("the %s profile got %d setting%s from its newer preset", jget_str(p, "name", ""), added, added == 1 ? "" : "s");
return added + dropped;
return added;
}
int detect_refresh_profiles(cJSON *profiles) {
-4
View File
@@ -25,7 +25,6 @@
#include "util.h"
#include "watch.h"
#include "web.h"
#include "presets.h"
#define MONITOR_INTERVAL_SEC 3
/* Failed foreground reads in a row before game detection is given up. */
@@ -145,7 +144,6 @@ static void *app_main(void *arg) {
plat_notify("RommPS: the console's date and time aren't set. Set them in the console's Settings for syncing to work.");
}
presets_init();
config_load();
if (g_cfg.profiles_custom && detect_refresh_profiles(g_cfg.profiles) > 0) config_save();
state_load();
@@ -196,8 +194,6 @@ static void *app_main(void *arg) {
library_link_extras();
sleep(5); /* wait for the network after boot */
presets_check();
if (config_is_paired()) {
char base[512], version[32] = "";
config_lock();
-220
View File
@@ -1,220 +0,0 @@
#include "presets.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <time.h>
#include "platform.h"
#include "util.h"
#include "config.h"
#include "detect.h"
#include "http.h"
#include "monocypher-ed25519.h"
#include "update_keys.h"
#include "state.h"
#ifndef PRESETS_URL
#define PRESETS_URL "https://raw.githubusercontent.com/s0liton/RommPS/presets"
#endif
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static cJSON *g_file;
static char g_source[16] = "built-in";
static int g_checking;
static char g_error[256];
static time_t g_checked_at;
int presets_version_cmp(const char *a, const char *b) {
int x[4] = {0}, y[4] = {0};
sscanf(a, "%d.%d.%d.%d", &x[0], &x[1], &x[2], &x[3]);
sscanf(b, "%d.%d.%d.%d", &y[0], &y[1], &y[2], &y[3]);
for (int i = 0; i < 4; i++)
if (x[i] != y[i]) return x[i] < y[i] ? -1 : 1;
return 0;
}
static void download_path(const char *name, char *out, size_t n) {
char dir[PATH_MAX_LEN];
path_join(dir, sizeof dir, plat_data_dir(), "presets");
path_join(out, n, dir, name);
}
static cJSON *usable(cJSON *f, const char *from) {
const char *why = !cJSON_IsObject(f) ? "Not valid JSON"
: jget_num(f, "format", 0) != PRESETS_FORMAT ? "In a format this version cannot read"
: strcmp(jget_str(f, "console", ""), plat_info()->name) != 0 ? "Not for this console"
: presets_version_cmp(jget_str(f, "version", ""), "0") <= 0 ? "Missing its version value"
: !cJSON_IsArray(cJSON_GetObjectItemCaseSensitive(f, "emulators")) ? "Missing emulators section."
: NULL;
if (!why) return f;
LOGW("Presets: %s is %s; Not using it", from, why);
cJSON_Delete(f);
return NULL;
}
static cJSON *read_download(const char *name) {
char path[PATH_MAX_LEN];
download_path(name, path, sizeof path);
char *txt = read_file(path, NULL);
if (!txt) return NULL; /* A presets file was never downloaded. */
cJSON *f = cJSON_Parse(txt);
free(txt);
return usable(f, name);
}
static void remove_downloads(void) {
char path[PATH_MAX_LEN];
download_path("current.json", path, sizeof path);
unlink(path);
download_path("previous.json", path, sizeof path);
unlink(path);
}
void presets_init(void) {
const char *embedded = plat_info()->emulator_catalog;
cJSON *best = embedded ? usable (cJSON_Parse(embedded), "The built-in presets file") : NULL;
cJSON *dl = read_download("current.json");
if (!dl) dl = read_download("previous.json");
if (dl && (!best || presets_version_cmp(jget_str(dl, "version", ""), jget_str(best, "version", "")) > 0)) {
cJSON_Delete(best);
best = dl;
str_copy(g_source, sizeof g_source, "download");
} else if (dl) {
/* A newer RommPS brought newer presets than the last download */
LOGI("presets: built-in %s is newer than the download (%s), let's remove it", jget_str(best, "version", ""), jget_str(dl, "version", ""));
cJSON_Delete(dl);
remove_downloads();
}
pthread_mutex_lock(&g_lock);
g_file = best;
pthread_mutex_unlock(&g_lock);
LOGI("presets: %s (%s)", best ? jget_str(best, "version", "") : "none", g_source);
}
const cJSON *presets_catalog(void) {
pthread_mutex_lock(&g_lock);
const cJSON *c = cJSON_GetObjectItemCaseSensitive(g_file, "emulators");
pthread_mutex_unlock(&g_lock);
return c;
}
/* Where presets-<console-name>.json and .sig file are located. */
static const char *presets_url(void) {
const char *u = getenv("ROMM_SYNC_PRESETS_URL"); /* Used for tests. */
return u && *u ? u: PRESETS_URL;
}
static int fetch(const char *url, http_resp *r, char *err, size_t en) {
if (http_get_verified(url, r) != 0) snprintf( err, en, "Can't reach %s: %s", url, r->error);
else if (r->status != 200) snprintf(err, en, "%s answered with HTTP %ld", url, r->status);
else return 0;
return -1;
}
/* Check to see if the presets file is signed by our release key */
static int signed_by_us(const http_resp *file, const http_resp *sig) {
if (sig->len != 64) return 0;
for (size_t i = 0; i < sizeof UPDATE_KEYS / sizeof UPDATE_KEYS[0]; i++)
if (crypto_ed25519_check((const uint8_t *)sig->body, UPDATE_KEYS[i], (const uint8_t *)file->body, file->len) == 0)
return 1;
return 0;
}
static int save_download(const http_resp *file, char *err, size_t en) {
char cur[PATH_MAX_LEN], prev[PATH_MAX_LEN];
download_path("current.json", cur, sizeof cur);
download_path("previous.json", prev, sizeof prev);
mkdir_parent(cur);
if (file_exists(cur) && move_file(cur, prev) !=0) LOGW("presets: can't keep the previous download");
if (write_file_atomic(cur, file->body, file->len) == 0) return 0;
snprintf(err, en, "can't save the presets to %s", cur);
return -1;
}
static void apply(cJSON *f) {
pthread_mutex_lock(&g_lock);
g_file = f;
str_copy(g_source, sizeof g_source, "download");
pthread_mutex_unlock(&g_lock);
config_lock();
int changed = g_cfg.profiles_custom && detect_refresh_profiles(g_cfg.profiles) > 0;
if (changed) config_save();
config_unlock();
LOGI("presets: now %s%s", jget_str(f, "version", ""), changed ? ", saved profiles updated" : "");
}
static void *check_thread(void *arg) {
(void)arg;
char url[600], sig_url[600], have[32], err[256] = "";
http_resp file = {0}, sig = {0};
cJSON *f = NULL;
snprintf(url, sizeof url, "%s/presets-%s.json", presets_url(), plat_info()->name);
snprintf(sig_url, sizeof sig_url, "%s/presets-%s.sig", presets_url(), plat_info()->name);
if(fetch(url, &file, err, sizeof err) == 0 && fetch(sig_url, &sig, err, sizeof err) == 0) {
if (!signed_by_us(&file, &sig)) snprintf(err, sizeof err, "the presets signature doesn't match, so not using them");
else if (!(f = usable(cJSON_ParseWithLength(file.body, file.len), "the download")))
snprintf(err, sizeof err, "the published presets can't be used here");
}
if (f) {
pthread_mutex_lock(&g_lock);
str_copy(have, sizeof have, g_file ? jget_str(g_file, "version", "0") : "0");
pthread_mutex_unlock(&g_lock);
if (presets_version_cmp(jget_str(f, "version", ""), have) <= 0) {
LOGI("presets: %s is current", have);
cJSON_Delete(f);
} else if (save_download(&file, err, sizeof err) == 0) {
apply(f);
} else {
cJSON_Delete(f);
}
}
if (err[0]) LOGW("presets: %s", err);
http_resp_free(&file);
http_resp_free(&sig);
pthread_mutex_lock(&g_lock);
g_checking = 0;
g_checked_at = time(NULL);
str_copy(g_error, sizeof g_error, err);
pthread_mutex_unlock(&g_lock);
return NULL;
}
int presets_check(void) {
pthread_mutex_lock(&g_lock);
int busy = g_checking;
g_checking = 1;
pthread_mutex_unlock(&g_lock);
if (busy) return -1;
if (thread_start(check_thread, NULL) != 0) {
pthread_mutex_lock(&g_lock);
g_checking = 0;
str_copy(g_error, sizeof g_error, "cannot start the presets check");
pthread_mutex_unlock(&g_lock);
return -1;
}
return 0;
}
cJSON *presets_status(void) {
cJSON *j = cJSON_CreateObject();
pthread_mutex_lock(&g_lock);
cJSON_AddStringToObject(j, "version", g_file ? jget_str(g_file, "version", "") : "");
cJSON_AddStringToObject(j, "source", g_source);
cJSON_AddStringToObject(j, "state", g_checking ? "checking" : g_error[0] ? "error" : "idle");
cJSON_AddStringToObject(j, "error", g_error);
cJSON_AddNumberToObject(j, "checked_at", (double)g_checked_at);
pthread_mutex_unlock(&g_lock);
return j;
}
-23
View File
@@ -1,23 +0,0 @@
#ifndef ROMM_PRESETS_H
#define ROMM_PRESETS_H
#include "cJSON.h"
#define PRESETS_FORMAT 1
/* Pick up the latest valid presets. Called at startup. */
void presets_init(void);
/* The catalog currently in use. Newer file takes its place and the old one stays valid in case it's still being used. */
const cJSON *presets_catalog(void);
/* Looks for newer presets in the background, and downloads, verifies, and applies them.*/
int presets_check(void);
/* Tells preset version, source (built-in) or (downloaded), info about the state, errors, and last checked time */
cJSON *presets_status(void);
/* Compare the version code within date to see if this contains newer presets. */
int presets_version_cmp(const char *a, const char *b);
#endif
+8 -20
View File
@@ -94,12 +94,13 @@ cJSON *profiles_default(void) {
return defs;
}
/* The built-in catalog's setting for the same emulator and system: its
* platform's, else its profile's. *known says whether the catalog has the
* emulator at all (RetroArch and hand-made profiles aren't in it). */
static const cJSON *catalog_setting(const cJSON *p, const cJSON *e, const char *key, int *known) {
const cJSON *cat = cJSON_GetObjectItemCaseSensitive(detect_catalog_entry(jstr(p, NULL, "id", "")), "profile"), *ce, *v;
if (known) *known = cat != NULL;
/* A save state setting: the profile's own, else the built-in catalog's for
* the same emulator and system (profiles saved before these settings existed). */
static const cJSON *state_setting(const cJSON *p, const cJSON *e, const char *key) {
const cJSON *v = cJSON_GetObjectItemCaseSensitive(e, key);
if (!v) v = cJSON_GetObjectItemCaseSensitive(p, key);
if (v) return v;
const cJSON *cat = cJSON_GetObjectItemCaseSensitive(detect_catalog_entry(jstr(p, NULL, "id", "")), "profile"), *ce;
if (!cat) return NULL;
cJSON_ArrayForEach(ce, cJSON_GetObjectItemCaseSensitive(cat, "platforms")) {
if (strcmp(profiles_system_key(ce), profiles_system_key(e)) != 0) continue;
@@ -108,14 +109,6 @@ static const cJSON *catalog_setting(const cJSON *p, const cJSON *e, const char *
return cJSON_GetObjectItemCaseSensitive(cat, key);
}
/* A save state setting: the profile's own, else the built-in catalog's for
* the same emulator and system (profiles saved before these settings existed). */
static const cJSON *state_setting(const cJSON *p, const cJSON *e, const char *key) {
const cJSON *v = cJSON_GetObjectItemCaseSensitive(e, key);
if (!v) v = cJSON_GetObjectItemCaseSensitive(p, key);
return v ? v : catalog_setting(p, e, key, NULL);
}
static const char *state_str(const cJSON *p, const cJSON *e, const char *key, const char *def) {
const cJSON *v = state_setting(p, e, key);
return cJSON_IsString(v) ? v->valuestring : def;
@@ -631,12 +624,7 @@ int profiles_expand(const cJSON *profiles, profile_map **out) {
m->extra = profile_lists(p, "extra", profiles_system_key(e));
const cJSON *mg = state_setting(p, e, "mount_games");
m->mount_games = cJSON_IsTrue(mg) ? 1 : cJSON_IsString(mg) && !strcmp(mg->valuestring, "data") ? 2 : 0;
/* Whether its saves can sync at all is the preset's call, never a
* saved copy's: a preset turns them on once they're tested, or off
* when an emulator update breaks them. The user's say is "sync". */
int in_catalog = 0;
const cJSON *can = catalog_setting(p, e, "sync_saves", &in_catalog);
m->sync_saves = m->base_sync_saves = in_catalog ? !cJSON_IsFalse(can) : jbool(p, e, "sync_saves", 1);
m->sync_saves = m->base_sync_saves = jbool(p, e, "sync_saves", 1);
const char *mode = jstr(p, e, "sync", "all"); /* per platform, or the emulator's (before 1.2) */
m->sync_off = !strcmp(mode, "off");
m->sync_states = !m->sync_off && strcmp(mode, "saves") != 0;
-10
View File
@@ -25,7 +25,6 @@
#include "memcard.h"
#include "pair.h"
#include "platform.h"
#include "presets.h"
#include "profiles.h"
#include "ra2.h"
#include "romm.h"
@@ -975,15 +974,6 @@ static void route(request *r) {
else send_json(fd, 200, update_status());
return;
}
if (is_get && !strcmp(r->path, "/api/presets")) {
send_json(fd, 200, presets_status());
return;
}
if (is_post && !strcmp(r->path, "/api/presets/check")) {
if (presets_check() != 0) send_error(fd, 409, "a presets check is already running");
else send_json(fd, 200, presets_status());
return;
}
/* Autostart works before pairing too: RommPS sets it up and keeps it
* current whether or not RomM Sync is paired yet. */
if (is_get && !strcmp(r->path, "/api/autostart")) {
+1 -1
View File
@@ -46,7 +46,7 @@ def cards_cell(c):
def section(console, path):
cat = json.load(open(os.path.join(ROOT, path)))["emulators"]
cat = json.load(open(os.path.join(ROOT, path)))
lines = ["## %s" % console, ""]
for e in cat:
profile = e.get("profile", {})