Adding a system or emulator
You can add a system and its emulator to Leaf, or add an alternate core to an
existing system, by installing a content pak. It is a .pak whose
pak.json has a top-level provides object. It never
patches Leaf’s installed files: the core, metadata, and artwork stay inside the
pak, and uninstalling the pak removes its contribution on the same library
rescan.
The complete buildable example is ScummVM-pak. Clone that repository if you want a working starting point rather than an empty directory. The public content-pak contract is the final authority when this guide and the schema disagree.
Before you start
Section titled “Before you start”A content pak must:
- target a concrete platform such as
mlp1, notshared; - live under
Apps/<platform>/Name.pak/on the primary SD card; - declare
min_leaf_versionfor a Leaf release that supports content paks; - keep every executable, core,
.infofile, and icon inside the pak; - use pak-relative paths with no absolute path or
..component.
The last rule matters on the MLP1: the same card can mount at /mnt/sdcard or
/media/sdcard1 after a reboot. Leaf resolves pak-relative paths against the
pak’s current install location, so the contribution survives that swap.
Start with this layout
Section titled “Start with this layout”MySystem.pak/ pak.json art/ MYSYSTEM.png cores/ mycore_libretro.so info/ mycore_libretro.infoDo not add launch.sh for a pure content pak. It will contribute its system but
stay out of the Apps tab. Add an executable launch.sh only if the package also
has an app UI; that hybrid pak contributes content and appears in Apps.
Declare a new system and libretro core
Section titled “Declare a new system and libretro core”This is the minimal useful shape. Replace every example identifier and file with
your own; paths are relative to MySystem.pak/.
{ "name": "My System", "platform": "mlp1", "pak_version": "1.0.0", "min_leaf_version": "0.11.0", "provides": { "schema": 1, "systems": [ { "id": "MYSYSTEM", "name": "My System", "patterns": ["MYSYSTEM"], "extensions": ["rom"], "rom_root": "Roms/MYSYSTEM", "image_root": "Images/MYSYSTEM", "default_core": "mycore", "icon_flat": "art/MYSYSTEM.png", "icon_photographic": null } ], "system_extensions": [], "cores": [ { "id": "mycore", "display_name": "My Core", "type": "retroarch", "libretro_name": "mycore", "file_name": "cores/mycore_libretro.so", "info_name": "info/mycore_libretro.info", "config_folder": "MyCore", "supports_menu": true, "supports_savestate": false, "supports_disk_control": false } ] }}id is the catalog identity. patterns are ROM-folder names Leaf recognizes;
keep the canonical spelling only because matching is case-insensitive.
rom_root and image_root are the public SD-card folders. default_core must
name a core in the final merged catalog.
config_folder becomes a real directory under Saves/ and States/, so use a
single FAT32-safe component. Set the three capability flags from the emulator’s
actual behavior rather than assuming every libretro core supports them.
Ship a standalone emulator
Section titled “Ship a standalone emulator”Use a path core for an executable that accepts the selected game path. The
target must exist inside the pak and be executable:
{ "id": "my_standalone", "display_name": "My Standalone Emulator", "type": "path", "path": "bin/my-emulator", "supports_menu": false, "supports_savestate": false, "supports_disk_control": false}A third-party manifest cannot request direct DRM, legacy flat-core migration,
arcade name maps, or a core status. The forbidden fields are
requires_direct_drm, legacy_flat_core, name_map, and status; Leaf derives
runtime status from the installed files.
Add a core to an existing system
Section titled “Add a core to an existing system”Do not copy an existing system into systems[]. Use system_extensions[],
which can only append alternate cores:
{ "systems": [], "system_extensions": [ { "system_id": "SFC", "add_alternate_cores": ["my_snes_core"] } ], "cores": [ { "id": "my_snes_core", "display_name": "My SNES Core", "type": "retroarch", "libretro_name": "my_snes_core", "file_name": "cores/my_snes_core_libretro.so", "info_name": "info/my_snes_core_libretro.info", "config_folder": "MySnesCore" } ]}Content paks cannot replace a first-party system or change its default core.
Artwork and scraper metadata
Section titled “Artwork and scraper metadata”Every new system ships a flat PNG in icon_flat. icon_photographic may name a
second PNG or be null; when it is absent, Leaf uses the flat icon even under
the Photographic setting. A user-created Roms/<SYSTEM>/icon.png still wins
over both.
Optional system fields include:
screenscraper_platform_ids: ScreenScraper numeric platform IDs;group: the group Central Scrutinizer displays, such asComputer;bios_directory: the directory belowBIOS/used by the system;bios_notes: short descriptions of required user-supplied BIOS files.
How ScreenScraper IDs work
Section titled “How ScreenScraper IDs work”ScreenScraper gives each platform a numeric systemeid. Put that number in
screenscraper_platform_ids; it is not the Leaf system id, ROM-folder name,
or emulator core id. For example, ScreenScraper’s ScummVM platform is 123:
"screenscraper_platform_ids": [123]Use ScreenScraper’s official
Web API reference and its
systemesListe.php response to find the number for a platform. A system may
declare up to eight IDs when its games genuinely span multiple ScreenScraper
platforms. Leaf tries them in the declared order until it finds usable artwork,
so put the most specific match first. A wrong ID produces wrong or missing
matches and wastes the user’s request allowance. A new third-party system
should declare its mapping explicitly. When no IDs are declared, Leaf falls
back to its built-in mappings for release systems; if neither mapping exists,
it cannot start a scrape.
The platform ID answers where to search. The ROM filename normally answers
what to search for. Some systems instead use a small descriptor file whose
contents are the canonical game identity. Declare that separately with the
optional top-level content_scrape companion:
{ "content_scrape": { "schema": 1, "systems": [ { "id": "SCUMMVM", "name_source": "descriptor", "lookup_extension": "scummvm" } ] }}If Kings Quest 1.scummvm contains kq1, Leaf first searches for
kq1.scummvm under platform 123, then falls back to filename-based
candidates. The launcher title and artwork path still use Kings Quest 1.
Only use descriptor mode when the file contains a stable game identifier; its
lookup_extension must also appear in that system’s extensions list.
Record the source and licence of both artwork and emulator binaries in the repository. If the emulator licence requires corresponding source, publish the source for the exact shipped binary alongside the package.
Validate before installing
Section titled “Validate before installing”CI should fetch a pinned commit of the public
leaf-contracts
repository and validate the packaged tree, not only the source manifest. The
ScummVM reference shows this flow in its Makefile and workflow.
For a quick device iteration:
- Assemble
MySystem.pak/on your computer. - Copy that one directory to
Apps/mlp1/on the primary SD card. - Put ROMs in the declared
Roms/<SYSTEM>/folder. - In Leaf, press MENU, then choose Actions → Rescan Library.
The new system and its games appear in that same rescan. Removing the pak and rescanning removes the system contribution; it does not delete the user’s ROMs, images, saves, or states.
My pak installs but doesn’t add its system or core
Section titled “My pak installs but doesn’t add its system or core”First open its Pak Rat detail page. Leaf shows what the installed manifest says it provides and the newest matching catalog diagnostic. Developers can also inspect:
.umrk/<platform>/catalog/diagnostics.jsonCommon failures are:
- a declared file is missing, not regular, or not executable;
- a path is absolute, traverses with
.., or escapes through a symlink; - the pak is under
Apps/shared/or on a secondary card; - a system/core id, ROM folder, image folder, config folder, or
.infofilename collides with Leaf or another pak; - a system’s
default_corewas rejected or does not exist.
Leaf drops only the refused contribution, logs the reason, and continues with a valid catalog. The content-pak field reference lists the allowed fields and identifier rules.