From 6aeb6a4527f4a02cdbb664e46d910d59f90ea837 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 7 Sep 2026 21:38:41 -0400 Subject: [PATCH 1/4] docs: document the per-platform custom library structure Add a "Custom library structure" section to Folder Structure covering the template syntax, several templates per platform, relocation semantics and the gotchas, plus a `filesystem.structure` entry in the config reference. Co-Authored-By: Claude Opus 5 (1M context) --- docs/getting-started/folder-structure.md | 51 ++++++++++++++++++++++++ docs/reference/configuration-file.md | 16 ++++++++ 2 files changed, 67 insertions(+) diff --git a/docs/getting-started/folder-structure.md b/docs/getting-started/folder-structure.md index de81f2a0..494cdfb5 100644 --- a/docs/getting-started/folder-structure.md +++ b/docs/getting-started/folder-structure.md @@ -156,6 +156,57 @@ Some games come as **folders** instead of single files, which could include mult !!! note "Starting from scratch?" If you upload files through the web UI without any existing structure, it'll create **Structure A** on your behalf. +## Custom library structure + +By default, a platform's ROM folder is scanned one level deep: each top-level file is a game, and each top-level folder is a single multi-file game. If your library is organised more deeply, describe that shape with a **structure template** in [`config.yml`](../reference/configuration-file.md#filesystemstructure). Templates are opt-in per platform, keyed by the platform folder name, and platforms you leave out keep the default behaviour. + +### Syntax + +A template is a `/`-separated path, relative to the platform's ROM folder. RomM resolves the library root, the `roms_folder` and the platform directory on its own, so those never appear in a template (`{library}` and `{platform}` are rejected). + +- A bare section is a literal folder name, matched exactly. +- A section wrapped in braces is a macro. +- The **last** section must be a terminal macro: `{gameFile}` makes every file at that level its own game, `{gameDir}` makes every folder at that level a single multi-file game. +- Any other braced section (`{region}`, `{category}`, or whatever you want to call it) is a wildcard directory level: it matches any folder name and is purely organisational. + +```yaml +filesystem: + structure: + # roms/snes/USA/foo.sfc, roms/snes/Japan/bar.sfc + snes: "{region}/{gameFile}" + # roms/ps3/Disc/Game/, roms/ps3/PSN/Game/ -> each folder is one game + ps3: "{category}/{gameDir}" +``` + +Declaring `{gameDir}` explicitly is what keeps multi-disc and `cue`+`bin` games whole: you say file-or-folder, RomM doesn't guess. + +### Several templates for one platform + +A platform can declare a **list** of templates, and discovery is their union. That covers the mixed layout no single fixed-depth template can express: loose games at the platform root **and** games inside grouping subfolders below it. + +```yaml +filesystem: + structure: + nes: + - "{gameFile}" # roms/nes/game01.nes + - "{category}/{gameFile}" # roms/nes/Hacks/game03.nes +``` + +### Moving games around + +Game identity is content-based, so a template isn't a cage: move or rename a game within it and the next scan recognises it by its hashes and relocates the existing entry in place, so its saves, states, play history, favourites and collection membership follow it. Removing a platform's template is just another relocation, moving every game back to the platform root rather than re-importing it. + + +!!! warning "Relocation needs an identity" + Matching a moved file to its entry needs all three of its hashes (CRC, MD5, SHA-1), so hashing has to be on (see [`filesystem.skip_hash_calculation`](../reference/configuration-file.md#filesystemskip_hash_calculation)). Platforms RomM doesn't hash (Switch, PS3, PS4, the PC and mobile platforms) fall back to the title id read out of the binary. With neither available, or when two entries missing from the same platform share an identity, the file is imported as a new game and the old entry stays flagged as missing from the filesystem. + +### Notes + +- Hidden (dot-prefixed) folders are never descended into or surfaced. +- A folder a template descends into is a grouping level, not a game. With `{gameDir}` and `{category}/{gameFile}` declared together, a folder holding discovered games is a category, and only folders no template descends into stay multi-file games. +- A folder previously scanned as one multi-file game that a new template descends into leaves its old entry marked as missing from the filesystem (the scan log flags it). Delete the stale entry to clean up. +- Two files with the same name in different folders become distinct games. `gamelist.xml` matching is by filename, so both may match the same gamelist entry. + ## Naming convention Filenames are parsed for region, language, revision, and arbitrary tags, with both `[]` and `()` delimiters supported: diff --git a/docs/reference/configuration-file.md b/docs/reference/configuration-file.md index 21688d26..d54c21ba 100644 --- a/docs/reference/configuration-file.md +++ b/docs/reference/configuration-file.md @@ -148,6 +148,22 @@ filesystem: firmware_folder: "firmware" ``` +### `filesystem.structure` + +Describe a deeper library layout per platform, instead of the default one-level-deep scan (top-level file = game, top-level folder = multi-file game). Keys are platform folder names, values are one template or a list of them. A template is a `/`-separated path relative to the platform's ROM folder, ending in `{gameFile}` (each file is a game) or `{gameDir}` (each folder is one multi-file game); any other braced section matches any folder name. Platforms you omit keep the default behaviour. + +```yaml +filesystem: + structure: + snes: "{region}/{gameFile}" + ps3: "{category}/{gameDir}" + nes: + - "{gameFile}" + - "{category}/{gameFile}" +``` + +See [Folder Structure -> Custom library structure](../getting-started/folder-structure.md#custom-library-structure) for the full syntax and how moving games between folders is handled. + ### `filesystem.skip_hash_calculation` Skip hashing on low-power devices. You lose hash-based matching (RetroAchievements, Hasheous, PlayMatch) but scans run much faster. From e0be0ed707127787d7f8d26636b6c2dd61128d3c Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 7 Sep 2026 22:07:50 -0400 Subject: [PATCH 2/4] docs: correct what removing a structure template does Testing the feature against a live library showed RomM moves nothing when a platform's template is removed: the nested games are flagged missing and the folders that grouped them are picked up as multi-file games instead. Co-Authored-By: Claude Opus 5 (1M context) --- docs/getting-started/folder-structure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/folder-structure.md b/docs/getting-started/folder-structure.md index 494cdfb5..ea6edf3f 100644 --- a/docs/getting-started/folder-structure.md +++ b/docs/getting-started/folder-structure.md @@ -194,7 +194,7 @@ filesystem: ### Moving games around -Game identity is content-based, so a template isn't a cage: move or rename a game within it and the next scan recognises it by its hashes and relocates the existing entry in place, so its saves, states, play history, favourites and collection membership follow it. Removing a platform's template is just another relocation, moving every game back to the platform root rather than re-importing it. +Game identity is content-based, so a template isn't a cage: move or rename a game within it and the next scan recognises it by its hashes and relocates the existing entry in place, so its saves, states, play history, favourites and collection membership follow it. Removing a platform's template moves nothing on disk: RomM goes back to scanning the platform root one level deep, so the games below it are flagged as missing from the filesystem, and the folders that used to group them are picked up as multi-file games instead (a dot-prefixed folder the template skipped can turn up as one too). Flatten the library out yourself and the next scan matches each game by hash and relocates it rather than importing a duplicate. !!! warning "Relocation needs an identity" From 95e40dc2af74129839a3b2971962e1878cb41605 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Tue, 8 Sep 2026 07:31:16 -0400 Subject: [PATCH 3/4] docs: note how exports and gamelist matching handle nested games Matching a gamelist entry now keys on its path relative to the platform folder, falling back to a bare file name, and both exporters write paths (and media) that follow the structure. Co-Authored-By: Claude Opus 5 (1M context) --- docs/getting-started/folder-structure.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/folder-structure.md b/docs/getting-started/folder-structure.md index ea6edf3f..423b4c9b 100644 --- a/docs/getting-started/folder-structure.md +++ b/docs/getting-started/folder-structure.md @@ -205,7 +205,8 @@ Game identity is content-based, so a template isn't a cage: move or rename a gam - Hidden (dot-prefixed) folders are never descended into or surfaced. - A folder a template descends into is a grouping level, not a game. With `{gameDir}` and `{category}/{gameFile}` declared together, a folder holding discovered games is a category, and only folders no template descends into stay multi-file games. - A folder previously scanned as one multi-file game that a new template descends into leaves its old entry marked as missing from the filesystem (the scan log flags it). Delete the stale entry to clean up. -- Two files with the same name in different folders become distinct games. `gamelist.xml` matching is by filename, so both may match the same gamelist entry. +- Two files with the same name in different folders become distinct games. A `gamelist.xml` entry is matched to one of them by its `` relative to the platform folder, and an entry carrying only a bare file name still matches as long as a single game has that name. +- Exported metadata follows the structure too: `gamelist.xml` and `metadata.pegasus.txt` entries carry each game's path relative to the platform folder, and exported media mirrors those folders. ## Naming convention From 7e9d2d369da8c1e90f447fb08f5eaa2f06fd0aab Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 10 Sep 2026 07:26:55 -0400 Subject: [PATCH 4/4] docs: rewrite the structure docs for the single-layout template syntax Upstream made filesystem.structure the one library layout: {gameFile} and {gameDir} collapsed into a single {game} terminal, templates now resolve from the library root with {platform} spelled out, and roms_folder/firmware_folder are retired keys that stop startup. Structure B is no longer auto-detected either, so it needs declaring. Every example here would have failed validation and exited 3, so rewrite the syntax, document the reserved default and firmware keys, and add the constraints that were missing: case-insensitive platform keys, the platform-dir agreement rule, the firmware template's literals-only levels, and the upload rejection when every template for a platform carries a wildcard level. Also correct two inversions: RomM stats the entry to tell a game from a multi-file game rather than taking your word for it, and dropping an override falls back to structure.default rather than to a one-level scan. Co-Authored-By: Claude Opus 5 (1M context) --- docs/getting-started/folder-structure.md | 49 ++++++++++++++++-------- docs/reference/configuration-file.md | 40 ++++++++----------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/docs/getting-started/folder-structure.md b/docs/getting-started/folder-structure.md index 423b4c9b..c184684b 100644 --- a/docs/getting-started/folder-structure.md +++ b/docs/getting-started/folder-structure.md @@ -7,7 +7,7 @@ description: How to organise your library on disk # Folder Structure -RomM expects your library to be organised in one of two layouts. It tries **Structure A** first, and falls back to **Structure B** if A isn't found. This auto-detection is per-library (not per-platform), so if you don't pick one up front, just arrange files the way you prefer and it'll figure it out. +RomM expects your library to be organised in one of two layouts. **Structure A** is what it scans unless told otherwise. **Structure B** is not auto-detected, so a library laid out that way has to declare it as a [structure template](#custom-library-structure) in `config.yml`, and RomM refuses to start with the two lines you need if it spots that layout undeclared. ## The two layouts @@ -20,13 +20,22 @@ Both layouts separate ROMs from BIOS files, and they differ on whether the split /bios/{platform}/ ``` -- **Structure B (fallback)**: one folder per platform at the top, `roms/` and `bios/` inside each +- **Structure B (opt-in)**: one folder per platform at the top, `roms/` and `bios/` inside each ```text /{platform}/roms/ /{platform}/bios/ ``` +Structure B is declared with two templates, and the [custom library structure](#custom-library-structure) section below covers the syntax: + +```yaml +filesystem: + structure: + default: "{platform}/roms/{game}" + firmware: "{platform}/bios" +``` + As the BIOS/firmware tree is **optional**, only platforms that require firmware for emulation need it. ### Mount point @@ -51,7 +60,7 @@ Some games come as **folders** instead of single files, which could include mult - +
Structure A (recommended)Structure B (fallback)Structure B (opt-in)
@@ -158,43 +167,50 @@ Some games come as **folders** instead of single files, which could include mult ## Custom library structure -By default, a platform's ROM folder is scanned one level deep: each top-level file is a game, and each top-level folder is a single multi-file game. If your library is organised more deeply, describe that shape with a **structure template** in [`config.yml`](../reference/configuration-file.md#filesystemstructure). Templates are opt-in per platform, keyed by the platform folder name, and platforms you leave out keep the default behaviour. +Both layouts above are **structure templates**, and so is anything deeper. `filesystem.structure` in [`config.yml`](../reference/configuration-file.md#filesystemstructure) holds them: `default` is the library-wide ROM layout (`roms/{platform}/{game}` unless you say otherwise), `firmware` is the firmware one (`bios/{platform}`), and any other key overrides the ROM layout for one platform. ### Syntax -A template is a `/`-separated path, relative to the platform's ROM folder. RomM resolves the library root, the `roms_folder` and the platform directory on its own, so those never appear in a template (`{library}` and `{platform}` are rejected). +A template is a `/`-separated path relative to the **library root**, the folder you mount as `/romm/library`. A bare section is a literal folder name, matched exactly, and a section wrapped in braces is a macro. -- A bare section is a literal folder name, matched exactly. -- A section wrapped in braces is a macro. -- The **last** section must be a terminal macro: `{gameFile}` makes every file at that level its own game, `{gameDir}` makes every folder at that level a single multi-file game. +- `{platform}` marks the platform folder, and every section before it has to be a literal so there is one known folder to enumerate platforms in. A per-platform override may spell that folder out by name instead, which is how its key already reads (`roms/ps3/{category}/{game}` under the `ps3` key). +- `{game}` is the **terminal** and has to be the last section. It marks the level where a game begins, and at that level a file is a game of its own while a folder is one multi-file game, so multi-disc and `cue`+`bin` games stay whole without you declaring anything. - Any other braced section (`{region}`, `{category}`, or whatever you want to call it) is a wildcard directory level: it matches any folder name and is purely organisational. +- `{library}` is rejected, because a template is already relative to the library root. ```yaml filesystem: structure: + # The defaults, spelled out + default: "roms/{platform}/{game}" + firmware: "bios/{platform}" # roms/snes/USA/foo.sfc, roms/snes/Japan/bar.sfc - snes: "{region}/{gameFile}" + snes: "roms/{platform}/{region}/{game}" # roms/ps3/Disc/Game/, roms/ps3/PSN/Game/ -> each folder is one game - ps3: "{category}/{gameDir}" + ps3: "roms/{platform}/{category}/{game}" ``` -Declaring `{gameDir}` explicitly is what keeps multi-disc and `cue`+`bin` games whole: you say file-or-folder, RomM doesn't guess. +Platform keys are matched case-insensitively, like `system.platforms`, so `Atari - 2600` and `atari - 2600` name the same platform. `default` and `firmware` are reserved, and a platform folder named either is read as the layout key rather than as an override. + +The `firmware` template takes only literal folder names around `{platform}`, no wildcard levels and no `{game}`, because it points at a folder rather than at a set of games. + +Every per-platform override has to agree with `default` on where the platform folder itself sits, since platform discovery enumerates a single folder. Pairing `default: "roms/{platform}/{game}"` with `snes: "games/{platform}/{game}"` is refused at startup. ### Several templates for one platform -A platform can declare a **list** of templates, and discovery is their union. That covers the mixed layout no single fixed-depth template can express: loose games at the platform root **and** games inside grouping subfolders below it. +A platform can declare a **list** of templates, and discovery is their union. That covers the mixed layout no single fixed-depth template can express: loose games directly in the platform folder **and** games inside grouping subfolders below it. ```yaml filesystem: structure: nes: - - "{gameFile}" # roms/nes/game01.nes - - "{category}/{gameFile}" # roms/nes/Hacks/game03.nes + - "roms/{platform}/{game}" # roms/nes/game01.nes + - "roms/{platform}/{category}/{game}" # roms/nes/Hacks/game03.nes ``` ### Moving games around -Game identity is content-based, so a template isn't a cage: move or rename a game within it and the next scan recognises it by its hashes and relocates the existing entry in place, so its saves, states, play history, favourites and collection membership follow it. Removing a platform's template moves nothing on disk: RomM goes back to scanning the platform root one level deep, so the games below it are flagged as missing from the filesystem, and the folders that used to group them are picked up as multi-file games instead (a dot-prefixed folder the template skipped can turn up as one too). Flatten the library out yourself and the next scan matches each game by hash and relocates it rather than importing a duplicate. +Game identity is content-based, so a template isn't a cage: move or rename a game within it and the next scan recognises it by its hashes and relocates the existing entry in place, so its saves, states, play history, favourites and collection membership follow it. Removing a platform's override moves nothing on disk: the platform falls back to `structure.default`, so the games that no longer sit where that template expects them are flagged as missing from the filesystem, and the folders that used to group them are picked up as multi-file games instead. Flatten the library out yourself and the next scan matches each game by hash and relocates it rather than importing a duplicate. !!! warning "Relocation needs an identity" @@ -203,8 +219,9 @@ Game identity is content-based, so a template isn't a cage: move or rename a gam ### Notes - Hidden (dot-prefixed) folders are never descended into or surfaced. -- A folder a template descends into is a grouping level, not a game. With `{gameDir}` and `{category}/{gameFile}` declared together, a folder holding discovered games is a category, and only folders no template descends into stay multi-file games. +- A folder a template descends into is a grouping level, not a game. With `roms/{platform}/{game}` and `roms/{platform}/{category}/{game}` declared together, a folder holding discovered games is a category, and only folders no template descends into stay multi-file games. - A folder previously scanned as one multi-file game that a new template descends into leaves its old entry marked as missing from the filesystem (the scan log flags it). Delete the stale entry to clean up. +- Uploading through the web UI needs a folder RomM can derive, so a platform whose every template carries a wildcard level rejects uploads. Add those files from the filesystem and rescan the platform. - Two files with the same name in different folders become distinct games. A `gamelist.xml` entry is matched to one of them by its `` relative to the platform folder, and an entry carrying only a bare file name still matches as long as a single game has that name. - Exported metadata follows the structure too: `gamelist.xml` and `metadata.pegasus.txt` entries carry each game's path relative to the platform folder, and exported media mirrors those folders. diff --git a/docs/reference/configuration-file.md b/docs/reference/configuration-file.md index d54c21ba..67c75d6c 100644 --- a/docs/reference/configuration-file.md +++ b/docs/reference/configuration-file.md @@ -130,39 +130,31 @@ system: ## `filesystem` -### `filesystem.roms_folder` - -Override the default ROMs folder name (`roms`). - -```yaml -filesystem: - roms_folder: "my_roms" -``` - -### `filesystem.firmware_folder` - -Override the default BIOS/firmware folder name (`bios`). - -```yaml -filesystem: - firmware_folder: "firmware" -``` - ### `filesystem.structure` -Describe a deeper library layout per platform, instead of the default one-level-deep scan (top-level file = game, top-level folder = multi-file game). Keys are platform folder names, values are one template or a list of them. A template is a `/`-separated path relative to the platform's ROM folder, ending in `{gameFile}` (each file is a game) or `{gameDir}` (each folder is one multi-file game); any other braced section matches any folder name. Platforms you omit keep the default behaviour. +Describe the whole library layout. A template is a `/`-separated path relative to the library root, where `{platform}` marks the platform folder and `{game}` the level a game begins at (a file there is one game, a folder one multi-file game). Any other braced section is a wildcard level matching any folder name. + +- `default` is the library-wide ROM layout, `roms/{platform}/{game}` if unset. +- `firmware` is the firmware layout, `bios/{platform}` if unset, and it takes only literal folder names around `{platform}`. +- Any other key is a platform folder name (matched case-insensitively, like `system.platforms`) overriding the ROM layout for that platform, as one template or a list of them whose discovery is unioned. An override has to agree with `default` on where the platform folder sits. ```yaml filesystem: structure: - snes: "{region}/{gameFile}" - ps3: "{category}/{gameDir}" + default: "roms/{platform}/{game}" + firmware: "bios/{platform}" + snes: "roms/{platform}/{region}/{game}" + ps3: "roms/{platform}/{category}/{game}" nes: - - "{gameFile}" - - "{category}/{gameFile}" + - "roms/{platform}/{game}" + - "roms/{platform}/{category}/{game}" ``` -See [Folder Structure -> Custom library structure](../getting-started/folder-structure.md#custom-library-structure) for the full syntax and how moving games between folders is handled. +See [Folder Structure → Custom library structure](../getting-started/folder-structure.md#custom-library-structure) for the full syntax and how moving games between folders is handled. + + +!!! warning "`roms_folder` and `firmware_folder` were retired" + Each named one path segment that a template now spells out, so RomM refuses to start while either is set, printing the template that reproduces the layout. `roms_folder: "my_roms"` becomes `default: "my_roms/{platform}/{game}"`, and `firmware_folder: "firmware"` becomes `firmware: "firmware/{platform}"`. ### `filesystem.skip_hash_calculation`