-
Notifications
You must be signed in to change notification settings - Fork 39
Retro Games
Moonbase can turn a folder of ROMs into a browsable game library that plays right in the browser and in the Moonfin apps. Games play with zero setup by default, because the emulator files load from a public CDN.
- ROM: the game file itself, one per game. You put these on the server.
- System folder: one folder per console inside your game library. Its name tells Moonbase which console the games inside are for.
- Core: the emulator for one system. The web player fetches them from a public CDN unless you install them on the server.
- BIOS: a firmware file some systems need on top of the core, PlayStation being the common one. It sits loose in that system's folder.
- Save state: a snapshot of a game at an exact moment, stored on the server per user so it follows people between devices.
- EmulatorJS and native: the two ways a game can play. EmulatorJS runs in a web view and works everywhere. Native cores run inside the Android, desktop, iPhone and Apple TV apps and are faster.
- Turn the feature on: open the Moonbase plugin page on your server dashboard, go to the Games tab and tick Enable retro games. Retro games are off until you do, and every game endpoint returns nothing until then.
- Pick your game libraries under Game libraries on the same tab, then Save.
- Organize your ROMs into one folder per system, named from the naming table below.
That's it. Game info and box art are filled in automatically from public databases, and games appear in the Moonfin clients.
If you have not picked any libraries, the plugin falls back to auto-detecting libraries whose name contains "game", "rom", or "emulator".
Advanced: why games aren't in the normal library database
ROM files aren't a media type Jellyfin or Emby understand, so the plugin doesn't index games in the normal library database. It reads your game folders directly off disk each time.
Inside a game library, use one folder per system, and one folder (or loose file) per game:
Games/
SNES/ one folder per system
bios files here loose BIOS files for that system
Super Mario World/ one folder per game
Super Mario World.sfc
Chrono Trigger.sfc loose ROMs also work
Genesis/
Sonic.md
The game title comes from the game folder name, or the file name for loose ROMs.
The system folder name tells the plugin which console a game is for, and it's what lets the game databases find the correct box art and details. Names are matched loosely: capital letters, spaces, and dashes are ignored, so Super Nintendo, super-nintendo, and SuperNintendo all work the same.
| Console | Name the folder any of these |
|---|---|
| NES / Famicom |
NES, Famicom
|
| Super Nintendo |
SNES, Super Nintendo, Super Famicom
|
| Game Boy / Game Boy Color |
GB, Game Boy, GBC, Game Boy Color
|
| Game Boy Advance |
GBA, Game Boy Advance
|
| Sega Genesis / Mega Drive |
Genesis, Mega Drive, Sega Genesis
|
| Sega Master System |
Master System, SMS
|
| Sega Game Gear |
Game Gear, GG
|
| Nintendo 64 |
N64, Nintendo 64
|
| Nintendo DS |
NDS, Nintendo DS
|
| Virtual Boy | Virtual Boy |
| Atari 2600 | Atari 2600 |
| Atari 7800 | Atari 7800 |
| Atari Lynx | Lynx |
| WonderSwan | WonderSwan |
| Neo Geo Pocket | Neo Geo Pocket |
| PC Engine / TurboGrafx-16 |
PC Engine, TurboGrafx 16
|
| PlayStation |
PSX, PS1, PSOne, PlayStation
|
| PSP |
PSP, PlayStation Portable
|
| Arcade |
Arcade, MAME
|
If a folder name isn't recognized, the plugin falls back to guessing the console from the ROM file extension, and if that also fails it assumes NES. Naming the folder from the list avoids the guessing.
| System | Apple TV | Moonfin-Core | webOS / Tizen |
|---|---|---|---|
| NES / Famicom | Yes | Yes | Yes |
| SNES / Super Famicom | Yes | Yes | Yes |
| Game Boy / Game Boy Color | Yes | Yes | Yes |
| Game Boy Advance | Yes | Yes | Yes |
| Sega Genesis / Mega Drive | Yes | Yes | Yes |
| Sega Master System | Yes | Yes | Yes |
| Sega Game Gear | Yes | Yes | Yes |
| PlayStation | Yes | Yes | Yes |
| Nintendo 64 | No | Yes | Yes |
| Nintendo DS | No | Yes | Yes |
| PSP | No | Yes | No |
| Virtual Boy | No | Yes | Yes |
| Atari 2600 | No | Yes | Yes |
| Atari 7800 | No | Yes | Yes |
| Atari Lynx | No | Yes | Yes |
| WonderSwan | No | Yes | Yes |
| Neo Geo Pocket | No | Yes | Yes |
| PC Engine / TurboGrafx-16 | No | Yes | Yes |
| Arcade | No | Yes | Yes |
The Moonfin-Core column covers the web, Android, desktop, and Android TV clients, which all play the same set. iOS plays the same systems except PSP. webOS / Tizen needs a supported TV version, covered below.
Multi-file disc sets (.cue with .bin, or .m3u multi-disc) don't work, and neither do heavier systems like Saturn, Sega CD, and 32X. Disc systems work with single-file images only (for example .chd or .pbp for PlayStation, .cso or .iso for PSP). PlayStation needs a BIOS placed in its system folder.
How a game plays depends on the client platform:
- Android and desktop play through native libretro cores by default, which run far faster than a web view. Cores are downloaded in the app under Settings > Playback > Emulator Cores (macOS ships them all in the bundle). You can switch to the EmulatorJS web player with the Native Emulation toggle, except on Linux, which has no web view and always plays native.
- iOS bundles a fixed set of native cores (NES, SNES, Game Boy and Color, Game Boy Advance, Genesis, Master System, Game Gear, PlayStation) and falls back to the web player for the rest, one game at a time. PSP plays nowhere on iOS.
- Apple TV plays only the eight bundled systems, through a native bridge. It has no usable web view and App Store rules don't allow downloading cores, so the set that ships is the set you get.
- Web and Smart-TV play everything through EmulatorJS in the browser or TV web view.
Advanced: platform notes
- Saves are kept separately per emulator. If you flip the Native Emulation toggle, each side keeps its own save states.
- On webOS and Tizen, games need webOS 5 or newer or Tizen 5 or newer. Emulator cores are WebAssembly, and the browsers on older TVs have no WebAssembly at all. Smart TVs also run the emulator without threading, so PSP doesn't play there even on a supported TV.
- Arcade runs on FBNeo natively on Android and desktop, and through EmulatorJS on iOS and web. It's deliberately left out of the iOS and Apple TV bundles, because the FBNeo license asks that frontends not redistribute the core. Apple TV has no web view, so there's no arcade path there at all.
- N64, PSP, and Nintendo DS need just-in-time compilation, which is only available on Android and desktop.
PSP games need cross-origin isolation to start in a browser. The player page gets it on its own, but Moonfin Web loads the player in an iframe, and an iframe can only be isolated when the page above it is. If PSP games stop with an error in Moonfin Web, open the Games tab on the Moonbase plugin page, tick Isolate the web app for PSP games, Save, then reload Moonfin Web.
- It's off by default, and it's Jellyfin only. Emby has no equivalent setting.
- It's applied per request, so there's no server restart, but an open Moonfin Web tab keeps the old behavior until you reload it.
- It only changes how the server sends Moonfin Web itself (the page at
/Moonfin/Web/). - Once it's on, anything cross-origin you've added to the web client has to allow isolation too, or the browser blocks it.
These controls apply to the native player on iOS, Android, and desktop. The EmulatorJS web player has its own on-screen controls and key bindings.
The menu pauses the game and offers save state, load state, press Start or Select, restart, fast forward, emulator settings, and exit. Exiting through the menu saves your state first, and the back button or key opens the menu instead of quitting the game, so progress isn't lost by accident.
Open it with:
| Input | Gesture |
|---|---|
| Controller | Hold Start for about 1.5 seconds. A quick press acts as a normal Start. On Xbox pads Start is the menu button, on PlayStation pads it's Options |
| Keyboard | Esc or Backspace |
| Touch | The menu icon in the top right |
| Mouse | The menu button in the top right |
| TV remote | The Back button |
| Apple TV | The Siri remote Back button |
In the menu, the d-pad moves, the bottom face button confirms, and the right face button goes back. On phones and tablets a game plays full screen in landscape.
| Key | Button |
|---|---|
| Arrow keys | D-pad |
| Z | B (bottom face) |
| X | A (right face) |
| A | Y (left face) |
| S | X (top face) |
| Q / W | L / R shoulders |
| Enter | Start |
| Right Shift | Select |
| Esc / Backspace | Open or close the menu |
These are RetroPad positions, so what each key does depends on the system. On NES, Z is B and X is A. Game Boy uses Z and X only. SNES uses all four face buttons.
Controllers work in-game on every platform with the native player. Face buttons map by position rather than by label, so the bottom face button is always B on Nintendo-style layouts.
Save states sync per user. When you save in the player, or exit through the in-game menu, your progress uploads to the server keyed to your account and the game, so it follows you to another device. Saves are capped at 32 MB each.
Battery saves are different. The in-game saves cartridges keep (.srm files) are not synced. They stay on the device, in storage the operating system may clear. When you want progress to follow you, use Save state in the in-game menu, or exit through that menu.
Advanced: where saves are stored on the server
Saves live inside the Moonfin plugin's own data folder:
<plugin data folder>/
saves/
<user id>/
<game id>.state save state
<game id>.settings emulator settings
The plugin data folder is the Moonfin folder under your server's plugins directory, the same for Jellyfin and Emby. See Data Locations.
A few things worth knowing about the file names:
- The user id is the account's id with the dashes removed, so each account gets its own folder and nobody can read anyone else's saves.
- The game id is the ROM's full path encoded as base64url. That means moving or renaming a ROM orphans its saves, because the new path encodes to a different id. Settle on a folder layout before people start playing.
- Native cores and the EmulatorJS web player write separate save files. Native states carry an
lr-prefix on the game id, so flipping the Native Emulation toggle hides the other side's saves rather than loading something the emulator can't read. The two formats aren't interchangeable. - Emulator settings are stored per emulator rather than per game:
moonfin-global.settingsfor the web player, andmoonfin-native-<core>.settingsfor each native core.
To back up saves, copy the saves folder. To move a user's progress to another account, copy their folder and rename it to the other account's id.
- ROMs stream to the player with range support, so large files can seek and resume.
-
.zipand.7zarchives work, with one ROM per archive. The server extracts them on the fly and leaves the original untouched. The.7zsupport uses the bundled SharpCompress library. - A BIOS is any non-ROM file at the top of a system folder, with an extension like
.bin,.bios,.rom,.img,.sys, or.bs. When a game needs one, the plugin lists it on the game and streams it to the player alongside the ROM.
Game info (overview, genre, developer, publisher, year, players, region) is filled in automatically from keyless public databases (the LaunchBox Games Database and libretro's databases). Box art and screenshots come from the libretro thumbnail library, downloaded and cached on the server.
This all happens entirely on the server, and the client never handles the ROM file itself. The file name is what the databases match on, so naming a ROM something they recognize is the best way to get the right result. Artwork that comes out wrong can be refreshed per game from the admin panel.
Advanced: how the artwork pipeline works
This pipeline is Jellyfin only. On Emby, box art is downloaded and cached the first time a client asks for it.
Box art and screenshots are cataloged on the server and handed to clients as a manifest, so a client asks for exactly the artwork it needs at the revision it already has rather than guessing at URLs. Thumbnails are derived and cached at the size and format actually requested.
Reconciliation runs when the plugin starts, after every completion of the server's own library refresh task, and from the file system itself. The plugin watches your configured game library folders for ROM changes, so a ROM you copy in picks up its artwork without restarting the plugin or the server. Filesystem events are debounced, so copying a whole romset settles into one reconciliation pass rather than one per file, and changing which game libraries are selected re-watches and re-scans the new roots on the spot. Turning Enable retro games on does the same, so artwork scanning starts right away instead of waiting for the next library scan or a restart.
Each pass also checks the artwork cache on disk. Artwork whose cached files were deleted outside the plugin is reopened and fetched again, so a cleared cache folder heals itself instead of leaving blank box art.
Artwork that came out as a false miss is retried. A cold metadata index at an earlier point could record a game as missing before its data had arrived, and the matching since found better names. Those older misses are reopened once so the corrected matching can look again.
Two admin controls sit on top of it. Artwork priority promotes already cataloged work into the order a client actually needs, and a per-artifact refresh reopens and reschedules one piece of artwork that came out wrong. The number of artwork responses streaming at once is bounded, and each user has their own concurrency limit, so one client can't starve the rest.
Advanced: arcade ROM matching
Arcade ROMs are ZIP sets rather than single files, and which core can run a given set is decided by the set's exact contents, not its name. The plugin reads each archive once, computes the hashes DAT matching needs, and checks them against FBNeo and MAME data files pinned locally on your server.
The whole path is offline on purpose. A client never picks a core from an external metadata service, and a data file you replace is picked up on the next lookup without restarting the plugin.
Upload a data file per core from the admin panel. The plugin reports separately when the file isn't valid XML, when it parses but holds no usable game sets, and when the core name isn't one it knows. Uploads are capped at 512 MB, since a real mame -listxml DAT runs past 200 MB. A single archive entry is capped at 512 MB while hashing too, so a zip bomb can't tie up a worker.
Put arcade sets in a folder named Arcade or MAME.
Advanced: per-game overrides
A game can be pinned to a specific core, or to the native or EmulatorJS backend, when the automatic choice gets it wrong. Both are stored per user.
"Cores" are the EmulatorJS runtime and the per-system emulator files, used wherever games play through the web player. The native cores the Android and desktop clients use are separate and are managed in the app under Settings > Playback > Emulator Cores.
The web player looks for its cores in this order:
- A custom URL you set in the admin panel
- Self-hosted files installed on your server
- The public EmulatorJS CDN (the default, so games work out of the box)
Self-hosting is worth it if you want games to work without reaching out to the CDN. From the admin panel, Install downloads the cores bundle in the background (around 290 MB), or Upload installs them from a zip you provide. The cores status shows whether they are installed or still downloading.
If a library isn't detected, the admin-only GET /Moonfin/Games/Debug endpoint dumps what the plugin sees: whether games are enabled, which library ids are configured, every library folder and whether it exists on disk, and the libraries it resolved as game libraries.
See the API Reference for the full endpoint list.
Moonfin does not provide or distribute games or BIOS files. Both come from a library you assemble on your own server, and you are responsible for holding the rights to anything you put there. Moonfin only plays files you already have, the same way it plays your films and music.
System names such as NES, SNES, Nintendo 64, Game Boy, Nintendo DS, Genesis, PlayStation, Atari, Neo Geo and WonderSwan are trademarks of their respective owners. They appear here only to describe which formats the emulator cores can read, and imply no affiliation with, sponsorship by, or endorsement from those companies. The cores themselves are third-party open-source projects, credited on the licenses screen in the app.
For server admins
Reference
For developers