diff --git a/README.md b/README.md index ab6c1178f..5be6ec4dc 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ $ igir --help | $$ | $$| \ | $$ | $$ $$ ROM collection manager | $$ | $$| \ | $$ | $$ $$ https://igir.io/ | $$ | $$ \$$$$ | $$ | $$$$$$$\ - _| $$_ | $$__| $$ _| $$_ | $$ | $$ v2.6.1 + _| $$_ | $$__| $$ _| $$_ | $$ | $$ v2.6.2 | $$ \ \$$ $$| $$ \| $$ | $$ \$$$$$$ \$$$$$$ \$$$$$$ \$$ \$$ @@ -247,6 +247,7 @@ Advanced usage: {pocket} The ROM's core-specific /Assets/* directory for the Analogue Pocket (e.g. "gb") {retrodeck} The ROM's emulator-specific /roms/* directory for the 'RetroDECK' image (e.g. "g b") + {romm} The ROM's manager-specific /roms/* directory for 'RomM' (e.g. "gb") {twmenu} The ROM's emulator-specific /roms/* directory for TWiLightMenu++ on the DSi/3DS (e.g. "gb") diff --git a/docs/output/tokens.md b/docs/output/tokens.md index e1e9ce545..cd8296993 100644 --- a/docs/output/tokens.md +++ b/docs/output/tokens.md @@ -93,6 +93,7 @@ To help sort ROMs into unique file structures for popular frontends & hardware, - `{onion}` the [OnionOS / GarlicOS](../usage/handheld/onionos.md) emulator's directory for the ROM - `{pocket}` the [Analogue Pocket](../usage/hardware/analogue-pocket.md) core's directory for the ROM - `{retrodeck}` the [RetroDECK](../usage/desktop/retrodeck.md) emulator's directory for the ROM +- `{romm}` the [RomM](../usage/desktop/romm.md) manager directory for the ROM - `{twmenu}` the [TWiLightMenu++](../usage/handheld/twmenu.md) emulator's directory for the ROM !!! tip diff --git a/docs/usage/desktop/romm.md b/docs/usage/desktop/romm.md new file mode 100644 index 000000000..af4fc47a0 --- /dev/null +++ b/docs/usage/desktop/romm.md @@ -0,0 +1,91 @@ +# RomM + +[RomM](https://github.com/rommapp/romm) is a web-based ROM management solution that allows you to scan, enrich, and browse your game collection with a clean and responsive interface. With support for multiple platforms, various naming schemes, and custom tags, RomM is a must-have for anyone who plays on emulators. + +## ROMs + +RomM uses its own [proprietary ROM folder structure](https://github.com/rommapp/romm/wiki/Supported-Platforms), so `igir` has a replaceable `{romm}` token to sort ROMs into the right place. See the [replaceable tokens page](../../output/tokens.md) for more information. + +You can run RomM using [Docker Compose](https://docs.docker.com/compose/). Create a file named `docker-compose.yml` with the following contents, but change all of the environment variables with the value of `CHANGEME!`: + +```yaml +version: "3" + +# https://github.com/rommapp/romm/blob/997d2cacd4b1980484eb63c2b3ffe65c83133966/examples/docker-compose.example.yml +services: + romm: + image: rommapp/romm + container_name: romm + restart: unless-stopped + environment: + - DB_HOST=romm-db + - DB_NAME=romm + - DB_USER=romm + - DB_PASSWD=CHANGEME! + - ENABLE_RESCAN_ON_FILESYSTEM_CHANGE=true + - ENABLE_SCHEDULED_RESCAN=true + - IGDB_CLIENT_ID=CHANGEME! + - IGDB_CLIENT_SECRET=CHANGEME! + - ROMM_AUTH_SECRET_KEY=CHANGEME! + - ROMM_AUTH_USERNAME=admin + - ROMM_AUTH_PASSWORD=CHANGEME! + volumes: + - ./romm/assets:/romm/assets + - ./romm/config:/romm/config + - ./romm/logs:/romm/logs + - ./romm/redis:/redis-data + - ./romm/resources:/romm/resources + - ./romm/roms:/romm/library/roms + ports: + - 80:8080 + depends_on: + - romm-db + romm-db: + image: mariadb:latest + container_name: romm-mariadb + restart: unless-stopped + environment: + - MARIADB_RANDOM_ROOT_PASSWORD=true + - MARIADB_DATABASE=romm + - MARIADB_USER=romm + - MARIADB_PASSWORD=CHANGEME! + expose: + - 3306 + volumes: + - ./romm-db:/var/lib/mysql +``` + +then, run Docker Compose as you would with any other config: + +```shell +docker compose up +``` + +This will create all of the local directories necessary. On your host machine (not from inside the container) you can sort your ROMs into the correct directories like this: + +=== ":simple-windowsxp: Windows" + + ```batch + igir copy zip test clean ^ + --dat "No-Intro*.zip" ^ + --input "ROMs\" ^ + --output "romm\roms\{romm}" + ``` + +=== ":simple-apple: macOS" + + ```shell + igir copy zip test clean \ + --dat "No-Intro*.zip" \ + --input "ROMs/" \ + --output "romm/roms/{romm}" + ``` + +=== ":simple-linux: Linux" + + ```shell + igir copy zip test clean \ + --dat "No-Intro*.zip" \ + --input "ROMs/" \ + --output "romm/roms/{romm}" + ``` diff --git a/mkdocs.yml b/mkdocs.yml index 21f842f67..e7eadd2e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - usage/desktop/retroarch.md - usage/desktop/retrodeck.md - usage/desktop/retropie.md + - usage/desktop/romm.md - usage/handheld/twmenu.md - FPGA: - usage/hardware/mister.md diff --git a/package-lock.json b/package-lock.json index b99f468c6..c8eeaf4c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "igir", - "version": "2.6.1", + "version": "2.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "igir", - "version": "2.6.1", + "version": "2.6.2", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 2eba9a9e0..86111dfc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "igir", - "version": "2.6.1", + "version": "2.6.2", "description": "🕹 A video game ROM collection manager to help filter, sort, patch, archive, and report on collections on any OS.", "keywords": [ "1g1r", diff --git a/src/constants.ts b/src/constants.ts index 7b8ebab48..0d7388f55 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,9 +42,22 @@ process.once('beforeExit', async () => { }); }); -const GLOBAL_CACHE_DIR = path.resolve(ROOT_DIR).startsWith(os.tmpdir()) - ? os.homedir() - : ROOT_DIR; +const GLOBAL_CACHE_FILE = [ + path.resolve(ROOT_DIR), + os.homedir(), + process.cwd(), +] + .filter((dir) => dir && !dir.startsWith(os.tmpdir())) + .map((dir) => path.join(dir, `${COMMAND_NAME}.cache`)) + .sort((a, b) => (fs.existsSync(a) ? 1 : 0) - (fs.existsSync(b) ? 1 : 0)) + .find((file) => { + try { + fsPoly.touchSync(file); + return true; + } catch { + return false; + } + }); /** * A static class of constants that are determined at startup, to be used widely. @@ -62,7 +75,7 @@ export default class Constants { static readonly GLOBAL_TEMP_DIR = GLOBAL_TEMP_DIR; - static readonly GLOBAL_CACHE_FILE = path.join(GLOBAL_CACHE_DIR, `${COMMAND_NAME}.cache`); + static readonly GLOBAL_CACHE_FILE = GLOBAL_CACHE_FILE; /** * A reasonable max of filesystem threads for operations such as: diff --git a/src/modules/argumentsParser.ts b/src/modules/argumentsParser.ts index 4f730c168..adbb16d9e 100644 --- a/src/modules/argumentsParser.ts +++ b/src/modules/argumentsParser.ts @@ -839,6 +839,7 @@ Advanced usage: {onion} The ROM's emulator-specific /Roms/* directory for OnionOS/GarlicOS (e.g. "GB") {pocket} The ROM's core-specific /Assets/* directory for the Analogue Pocket (e.g. "gb") {retrodeck} The ROM's emulator-specific /roms/* directory for the 'RetroDECK' image (e.g. "gb") + {romm} The ROM's manager-specific /roms/* directory for 'RomM' (e.g. "gb") {twmenu} The ROM's emulator-specific /roms/* directory for TWiLightMenu++ on the DSi/3DS (e.g. "gb") Example use cases: diff --git a/src/polyfill/fsPoly.ts b/src/polyfill/fsPoly.ts index 76bd71200..afc1debb5 100644 --- a/src/polyfill/fsPoly.ts +++ b/src/polyfill/fsPoly.ts @@ -389,6 +389,22 @@ export default class FsPoly { await util.promisify(fs.close)(file); } + static touchSync(filePath: string): void { + const dirname = path.dirname(filePath); + if (!fs.existsSync(dirname)) { + fs.mkdirSync(dirname, { recursive: true }); + } + + // Create the file if it doesn't already exist + const file = fs.openSync(filePath, 'a'); + + // Ensure the file's `atime` and `mtime` are updated + const date = new Date(); + fs.futimesSync(file, date, date); + + fs.closeSync(file); + } + static async walk(pathLike: PathLike, callback?: FsWalkCallback): Promise { let output: string[] = []; diff --git a/src/types/cache.ts b/src/types/cache.ts index 955aeac6f..7a5ecedbb 100644 --- a/src/types/cache.ts +++ b/src/types/cache.ts @@ -170,17 +170,19 @@ export default class Cache { return this; } - const cacheData = JSON.parse( - await util.promisify(fs.readFile)(this.filePath, { encoding: Cache.BUFFER_ENCODING }), - ) as CacheData; - const compressed = Buffer.from(cacheData.data, Cache.BUFFER_ENCODING); - const decompressed = await util.promisify(zlib.inflate)(compressed); - const keyValuesObject = JSON.parse(decompressed.toString(Cache.BUFFER_ENCODING)); - const keyValuesEntries = Object.entries(keyValuesObject) as [string, V][]; - this.keyValues = new Map(keyValuesEntries); - if (this.maxSize !== undefined) { - this.keyOrder = new Set(Object.keys(keyValuesObject)); - } + try { + const cacheData = JSON.parse( + await util.promisify(fs.readFile)(this.filePath, { encoding: Cache.BUFFER_ENCODING }), + ) as CacheData; + const compressed = Buffer.from(cacheData.data, Cache.BUFFER_ENCODING); + const decompressed = await util.promisify(zlib.inflate)(compressed); + const keyValuesObject = JSON.parse(decompressed.toString(Cache.BUFFER_ENCODING)); + const keyValuesEntries = Object.entries(keyValuesObject) as [string, V][]; + this.keyValues = new Map(keyValuesEntries); + if (this.maxSize !== undefined) { + this.keyOrder = new Set(Object.keys(keyValuesObject)); + } + } catch { /* empty */ } return this; } diff --git a/src/types/gameConsole.ts b/src/types/gameConsole.ts index e5d14dbc0..fff149c27 100644 --- a/src/types/gameConsole.ts +++ b/src/types/gameConsole.ts @@ -50,6 +50,10 @@ interface OutputTokens { // @see https://github.com/XargonWan/RetroDECK/blob/main/es-configs/es_systems.xml retrodeck?: string, + // RomM ROMs go in the /romm/library/{romm} directory: + // @see https://github.com/rommapp/romm/wiki/Supported-Platforms + romm?: string, + // TWiLightMenu++ Roms go into the /roms subfolder on the 3DS/DSi SD card // @see https://github.com/DS-Homebrew/TWiLightMenu/tree/master/7zfile/roms twmenu?: string, @@ -82,10 +86,12 @@ export default class GameConsole { mister: 'Amstrad', onion: 'CPC', retrodeck: 'amstradcpc', + romm: 'acpc', twmenu: 'cpc', }), new GameConsole(/PCW/i, [/* unknown */], { mister: 'AmstradPCW', + romm: 'amstrad-pcw', }), // Apple new GameConsole(/Apple.*I/i, [/* unknown */], { @@ -96,10 +102,12 @@ export default class GameConsole { emulationstation: 'apple2', mister: 'Apple-II', retrodeck: 'apple2', + romm: 'appleii', }), new GameConsole(/Apple.*IIGS/i, ['.2mg'], { emulationstation: 'apple2gs', retrodeck: 'apple2gs', + romm: 'apple-iigs', }), // Arduboy new GameConsole(/Arduboy/i, ['.arduboy', '.hex'], { @@ -109,6 +117,7 @@ export default class GameConsole { mister: 'Arduboy', pocket: 'arduboy', retrodeck: 'arduboy', + romm: 'arduboy', }), // Atari new GameConsole(/800|8-bit Family/, ['.atr', '.atx'], { @@ -118,6 +127,7 @@ export default class GameConsole { mister: 'ATARI800', onion: 'EIGHTHUNDRED', retrodeck: 'atari800', + romm: 'atari8bit', }), new GameConsole(/2600/, ['.a26', '.act', '.pb', '.tv', '.tvr', '.mn', '.cv', '.eb', '.ef', '.efr', '.ua', '.x07', '.sb'], { adam: 'A2600', @@ -129,6 +139,7 @@ export default class GameConsole { onion: 'ATARI', pocket: '2600', retrodeck: 'atari2600', + romm: 'atari2600', twmenu: 'a26', }), new GameConsole(/5200/, ['.a52'], { @@ -139,6 +150,7 @@ export default class GameConsole { mister: 'Atari5200', onion: 'FIFTYTWOHUNDRED', retrodeck: 'atari5200', + romm: 'atari5200', twmenu: 'a52', }), new GameConsole(/7800/, ['.a78'], { @@ -150,6 +162,7 @@ export default class GameConsole { onion: 'SEVENTYEIGHTHUNDRED', pocket: '7800', retrodeck: 'atari7800', + romm: 'atari7800', twmenu: 'a78', }), new GameConsole(/Jaguar/i, ['.j64'], { @@ -158,6 +171,7 @@ export default class GameConsole { jelos: 'atarijaguar', onion: 'JAGUAR', retrodeck: 'atarijaguar', + romm: 'jaguar', }), new GameConsole(/Lynx/i, ['.lnx', '.lyx'], { adam: 'LYNX', @@ -169,6 +183,7 @@ export default class GameConsole { miyoocfw: 'LYNX', onion: 'LYNX', retrodeck: 'atarilynx', + romm: 'lynx', }), new GameConsole(/Atari.*ST/i, ['.msa', '.st', '.stx'], { batocera: 'atarist', @@ -177,6 +192,7 @@ export default class GameConsole { mister: 'AtariST', onion: 'ATARIST', retrodeck: 'atarist', + romm: 'atari-st', }), // Bally new GameConsole(/Astrocade/i, [/* '.bin' */], { @@ -184,6 +200,7 @@ export default class GameConsole { emulationstation: 'astrocde', mister: 'Astrocade', retrodeck: 'astrocde', + romm: 'astrocade', }), // Bandai new GameConsole(/Super ?Vision 8000/i, [/* unknown */], { @@ -203,6 +220,7 @@ export default class GameConsole { onion: 'WS', pocket: 'wonderswan', retrodeck: 'wonderswan', + romm: 'wonderswan', twmenu: 'ws', }), new GameConsole(/WonderSwan Color/i, ['.wsc'], { @@ -216,6 +234,7 @@ export default class GameConsole { onion: 'WS', pocket: 'wonderswan', retrodeck: 'wonderswancolor', + romm: 'wonderswan-color', twmenu: 'ws', }), // Bit Corporation @@ -224,6 +243,7 @@ export default class GameConsole { emulationstation: 'gamate', mister: 'Gamate', pocket: 'gamate', + romm: 'gamate', }), // Capcom // TODO(cemmer): CPS1, CPS2, CPS3 @@ -246,6 +266,7 @@ export default class GameConsole { onion: 'AMIGA', pocket: 'amiga', retrodeck: 'amiga', + romm: 'amiga', }), new GameConsole(/Amiga CD32/i, [/* '.bin', '.cue' */], { adam: 'AMIGA', @@ -255,16 +276,19 @@ export default class GameConsole { mister: 'Amiga', onion: 'AMIGACD', retrodeck: 'amigacd32', + romm: 'amiga-cd32', }), new GameConsole(/Amiga CDTV/i, [/* '.bin', '.cue' */], { adam: 'AMIGA', batocera: 'amigacdtv', emulationstation: 'cdtv', retrodeck: 'cdtv', + romm: 'commodore-cdtv', }), new GameConsole(/Commodore C?16/i, [/* unknown */], { jelos: 'c16', mister: 'C16', + romm: 'c16', }), new GameConsole(/Commodore C?64/i, ['.crt', '.d64', '.t64'], { adam: 'C64', @@ -274,20 +298,24 @@ export default class GameConsole { mister: 'C64', onion: 'COMMODORE', retrodeck: 'c64', + romm: 'c64', }), new GameConsole(/Commodore C?128/i, [/* unknown */], { batocera: 'c128', jelos: 'c128', mister: 'C128', + romm: 'c64', }), new GameConsole(/Plus.*4/i, [], { emulationstation: 'plus4', retrodeck: 'plus4', + romm: 'c-plus-4', }), new GameConsole(/VIC.*20/i, [], { emulationstation: 'vic20', onion: 'VIC20', retrodeck: 'vic20', + romm: 'vic-20', }), // Coleco new GameConsole(/ColecoVision/i, ['.col'], { @@ -299,6 +327,7 @@ export default class GameConsole { onion: 'COLECO', pocket: 'coleco', retrodeck: 'colecovision', + romm: 'colecovision', twmenu: 'col', }), // Emerson @@ -321,6 +350,7 @@ export default class GameConsole { batocera: 'scv', emulationstation: 'scv', retrodeck: 'scv', + romm: 'epoch-super-cassette-vision', }), // Fairchild new GameConsole(/Channel F/i, ['.chf'], { @@ -331,6 +361,7 @@ export default class GameConsole { onion: 'FAIRCHILD', pocket: 'channel_f', retrodeck: 'channelf', + romm: 'fairchild-channel-f', }), // Funtech new GameConsole(/Super A'?Can/i, [/* '.bin' */], { @@ -346,6 +377,7 @@ export default class GameConsole { miyoocfw: 'VECTREX', onion: 'VECTREX', retrodeck: 'vectrex', + romm: 'vectrex', }), // Hartung new GameConsole(/Game Master/i, [/* '.bin' */], { @@ -356,6 +388,7 @@ export default class GameConsole { new GameConsole(/VC ?4000/i, [/* '.bin' */], { batocera: 'vc4000', mister: 'VC4000', + romm: 'vc-4000', }), // Magnavox new GameConsole(/Odyssey 2/i, [/* '.bin' */], { @@ -366,6 +399,7 @@ export default class GameConsole { onion: 'ODYSSEY', pocket: 'odyssey2', retrodeck: 'odyssey2', + romm: 'odyssey-2-slash-videopac-g7000', }), // Mattel new GameConsole(/Intellivision/i, ['.int'], { @@ -377,6 +411,7 @@ export default class GameConsole { onion: 'INTELLIVISION', pocket: 'intv', retrodeck: 'intellivision', + romm: 'intellivision', }), // Microsoft new GameConsole(/MSX/i, ['.mx1'], { @@ -387,6 +422,7 @@ export default class GameConsole { mister: 'MSX', onion: 'MSX', retrodeck: 'msx', + romm: 'msx', }), new GameConsole(/MSX2/i, ['.mx2'], { adam: 'MSX', @@ -396,6 +432,7 @@ export default class GameConsole { mister: 'MSX', onion: 'MSX', retrodeck: 'msx2', + romm: 'msx2', }), new GameConsole(/MSX2+/i, [], { adam: 'MSX', @@ -404,6 +441,7 @@ export default class GameConsole { mister: 'MSX', onion: 'MSX', retrodeck: 'msx2', + romm: 'msx2', }), new GameConsole(/MSX TurboR/i, [], { adam: 'MSX', @@ -411,17 +449,20 @@ export default class GameConsole { emulationstation: 'msx', mister: 'MSX', onion: 'MSX', - retrodeck: 'msx2', + retrodeck: 'msxturbor', + romm: 'msx', }), new GameConsole(/Xbox/i, [/* '.iso' */], { batocera: 'xbox', emulationstation: 'xbox', jelos: 'xbox', retrodeck: 'xbox', + romm: 'xbox', }), new GameConsole(/Xbox 360/i, [/* '.iso' */], { batocera: 'xbox360', emulationstation: 'xbox360', + romm: 'xbox360', }), // Mobile new GameConsole(/J2ME/i, ['.jar'], { @@ -431,6 +472,7 @@ export default class GameConsole { new GameConsole(/Palm OS/i, ['.pqa', '.prc'], { emulationstation: 'palm', retrodeck: 'palm', + romm: 'palm-os', }), new GameConsole(/Symbian/i, ['.sis', '.sisx', '.symbian'], { emulationstation: 'symbian', @@ -453,6 +495,7 @@ export default class GameConsole { onion: 'PCE', pocket: 'pce', retrodeck: 'pcengine', + romm: 'turbografx16--1', twmenu: 'tg16', }), new GameConsole(/(PC Engine|TurboGrafx) CD/i, [/* '.bin', '.cue' */], { @@ -466,6 +509,7 @@ export default class GameConsole { onion: 'PCECD', pocket: 'pcecd', retrodeck: 'pcenginecd', + romm: 'turbografx-16-slash-pc-engine-cd', }), new GameConsole(/SuperGrafx/i, ['.sgx'], { batocera: 'supergrafx', @@ -475,6 +519,7 @@ export default class GameConsole { onion: 'SGFX', pocket: 'pce', retrodeck: 'supergrafx', + romm: 'supergrafx', }), new GameConsole(/PC-88/i, ['.d88'], { batocera: 'pc88', @@ -483,6 +528,7 @@ export default class GameConsole { mister: 'PC8801', onion: 'PCEIGHTYEIGHT', retrodeck: 'pc88', + romm: 'pc-8800-series', }), new GameConsole(/PC-98/i, ['.d98'], { batocera: 'pc98', @@ -490,11 +536,13 @@ export default class GameConsole { jelos: 'pc98', onion: 'PCNINETYEIGHT', retrodeck: 'pc98', + romm: 'pc-9800-series', }), new GameConsole(/PC-FX/i, [], { emulationstation: 'pcfx', onion: 'PCFX', retrodeck: 'pcfx', + romm: 'pc-fx', }), // nesbox new GameConsole(/TIC-80/i, ['.tic'], { @@ -515,6 +563,7 @@ export default class GameConsole { onion: 'FDS', pocket: 'nes', retrodeck: 'fds', + romm: 'fds', }), new GameConsole(/Game (and|&) Watch/i, ['.mgw'], { adam: 'GW', @@ -524,12 +573,14 @@ export default class GameConsole { mister: 'GameNWatch', onion: 'GW', retrodeck: 'gameandwatch', + romm: 'game-and-watch', }), new GameConsole(/GameCube/i, [/* '.iso' */], { batocera: 'gc', emulationstation: 'gc', jelos: 'gamecube', retrodeck: 'gc', + romm: 'ngc', }), new GameConsole(/GB|Game ?Boy/i, ['.gb', '.sgb'], { adam: 'GB', @@ -543,6 +594,7 @@ export default class GameConsole { onion: 'GB', pocket: 'gb', retrodeck: 'gb', + romm: 'gb', twmenu: 'gb', }), // pocket:sgb for spiritualized1997 new GameConsole(/GBA|Game ?Boy Advance/i, ['.gba', '.srl'], { @@ -557,6 +609,7 @@ export default class GameConsole { onion: 'GBA', pocket: 'gba', retrodeck: 'gba', + romm: 'gba', twmenu: 'gba', }), new GameConsole(/GBC|Game ?Boy Color/i, ['.gbc'], { @@ -571,6 +624,7 @@ export default class GameConsole { onion: 'GBC', pocket: 'gbc', retrodeck: 'gbc', + romm: 'gbc', twmenu: 'gb', }), new GameConsole(/Nintendo 64|N64/i, ['.d64', '.n64', '.v64', '.z64'], { @@ -579,17 +633,20 @@ export default class GameConsole { jelos: 'n64', mister: 'N64', retrodeck: 'n64', + romm: 'n64', }), new GameConsole(/Nintendo 64DD|N64DD/i, ['.ndd'], { batocera: 'n64dd', emulationstation: 'n64dd', retrodeck: 'n64dd', + romm: 'nintendo-64dd', }), new GameConsole(/(\W|^)3DS(\W|$)|Nintendo 3DS/i, ['.3ds', '.3dsx'], { batocera: '3ds', emulationstation: 'n3ds', jelos: '3ds', retrodeck: 'n3ds', + romm: '3ds', }), new GameConsole(/(\W|^)NDS(\W|$)|Nintendo DS/i, ['.nds'], { batocera: 'nds', @@ -597,11 +654,13 @@ export default class GameConsole { jelos: 'nds', onion: 'NDS', retrodeck: 'nds', + romm: 'nds', twmenu: 'nds', }), new GameConsole(/(\W|^)NDSi(\W|$)|Nintendo DSi([Ww]are)?/i, [], { emulationstation: 'nds', retrodeck: 'nds', + romm: 'nintendo-dsi', twmenu: 'dsiware', }), // try to map DSiWare new GameConsole(/(\W|^)NES(\W|$)|Famicom|Nintendo Entertainment System/i, ['.nes', '.nez'], { @@ -616,6 +675,7 @@ export default class GameConsole { onion: 'FC', pocket: 'nes', retrodeck: 'nes', + romm: 'nes', twmenu: 'nes', }), new GameConsole(/Pokemon Mini/i, ['.min'], { @@ -630,6 +690,7 @@ export default class GameConsole { onion: 'POKE', pocket: 'poke_mini', retrodeck: 'pokemini', + romm: 'pokemon-mini', }), new GameConsole(/Satellaview/i, ['.bs'], { batocera: 'satellaview', @@ -639,6 +700,7 @@ export default class GameConsole { onion: 'SATELLAVIEW', pocket: 'snes', retrodeck: 'satellaview', + romm: 'satellaview', }), new GameConsole(/Sufami/i, [], { batocera: 'sufami', @@ -659,11 +721,13 @@ export default class GameConsole { onion: 'SFC', pocket: 'snes', retrodeck: 'snes', + romm: 'snes', twmenu: 'snes', }), new GameConsole(/Switch/i, ['.nca', '.nro', '.nso', '.nsp', '.xci'], { emulationstation: 'switch', retrodeck: 'switch', + romm: 'switch', }), new GameConsole(/Virtual Boy/i, ['.vb', '.vboy'], { adam: 'VB', @@ -674,18 +738,21 @@ export default class GameConsole { minui: 'Virtual Boy (VB)', onion: 'VB', retrodeck: 'virtualboy', + romm: 'virtualboy', }), new GameConsole(/Wii/i, [/* '.iso' */], { batocera: 'wii', emulationstation: 'wii', jelos: 'wii', retrodeck: 'wii', + romm: 'wii', }), new GameConsole(/Wii ?U/i, ['.rpx', '.wua', '.wud', '.wux'], { batocera: 'wiiu', emulationstation: 'wiiu', jelos: 'wiiu', retrodeck: 'wiiu', + romm: 'wiiu', }), // Panasonic new GameConsole(/3DO/i, [/* '.bin', '.cue' */], { @@ -694,6 +761,7 @@ export default class GameConsole { jelos: '3do', onion: 'PANASONIC', retrodeck: '3do', + romm: '3do', }), // Philips new GameConsole(/CD[ -]?i/i, [/* '.bin', '.cue' */], { @@ -708,6 +776,7 @@ export default class GameConsole { mister: 'Odyssey2', onion: 'VIDEOPAC', retrodeck: 'videopac', + romm: 'odyssey-2-slash-videopac-g7000', }), // RCA new GameConsole(/Studio (2|II)/i, [/* '.bin' */], { @@ -723,12 +792,14 @@ export default class GameConsole { mister: 'S32X', onion: 'THIRTYTWOX', retrodeck: 'sega32x', + romm: 'sega32', }), new GameConsole(/Dreamcast/i, [/* '.bin', '.cue' */], { batocera: 'dreamcast', emulationstation: 'dreamcast', jelos: 'dreamcast', retrodeck: 'dreamcast', + romm: 'dc', }), new GameConsole(/Game Gear/i, ['.gg'], { adam: 'GG', @@ -742,6 +813,7 @@ export default class GameConsole { onion: 'GG', pocket: 'gg', retrodeck: 'gamegear', + romm: 'gamegear', twmenu: 'gg', }), new GameConsole(/Master System/i, ['.sms'], { @@ -756,6 +828,7 @@ export default class GameConsole { onion: 'MS', pocket: 'sms', retrodeck: 'mastersystem', + romm: 'sms', twmenu: 'sms', }), new GameConsole(/(Mega|Sega) CD/i, [/* '.bin', '.cue' */], { @@ -768,6 +841,7 @@ export default class GameConsole { miyoocfw: 'SMD', onion: 'SEGACD', retrodeck: 'segacd', + romm: 'segacd', }), new GameConsole(/Mega Drive|Genesis/i, ['.gen', '.md', '.mdx', '.sgd', '.smd'], { adam: 'MD', @@ -781,6 +855,7 @@ export default class GameConsole { onion: 'MD', pocket: 'genesis', retrodeck: 'megadrive', + romm: 'genesis-slash-megadrive', twmenu: 'gen', }), new GameConsole(/Saturn/i, [/* '.bin', '.cue' */], { @@ -788,6 +863,7 @@ export default class GameConsole { emulationstation: 'saturn', jelos: 'saturn', retrodeck: 'saturn', + romm: 'saturn', }), new GameConsole(/SG[ -]?1000/i, ['.sc', '.sg'], { adam: 'SG1000', @@ -798,11 +874,13 @@ export default class GameConsole { onion: 'SEGASGONE', pocket: 'sg1000', retrodeck: 'sg-1000', + romm: 'sg1000', twmenu: 'sg', }), // Sharp new GameConsole(/MZ/i, [], { mister: 'SharpMZ', + romm: 'sharp-mz-2200', }), new GameConsole(/X1/i, ['.2d', '.2hd', '.dx1', '.tfd'], { batocera: 'x1', @@ -810,6 +888,7 @@ export default class GameConsole { jelos: 'x1', onion: 'XONE', retrodeck: 'x1', + romm: 'x1', }), new GameConsole(/X68000/i, [], { batocera: 'x68000', @@ -818,20 +897,23 @@ export default class GameConsole { mister: 'X68000', onion: 'X68000', retrodeck: 'x68000', + romm: 'sharp-x68000', }), // Sinclair new GameConsole(/ZX[ -]?80/i, [], { emulationstation: 'zx81', mister: 'ZX81', + onion: 'ZXEIGHTYONE', retrodeck: 'zx81', + romm: 'sinclair-zx81', }), new GameConsole(/ZX[ -]?81/i, [], { batocera: 'zx81', emulationstation: 'zx81', jelos: 'zx81', mister: 'ZX81', - onion: 'ZXEIGHTYONE', retrodeck: 'zx81', + romm: 'sinclair-zx81', }), new GameConsole(/ZX[ -]?Spectrum/i, ['.scl', '.szx', '.z80'], { adam: 'ZX', @@ -841,6 +923,7 @@ export default class GameConsole { mister: 'Spectrum', onion: 'ZXS', retrodeck: 'zxspectrum', + romm: 'zxs', }), // SNK new GameConsole(/Neo ?Geo/i, [], { @@ -853,6 +936,7 @@ export default class GameConsole { onion: 'NEOGEO', pocket: 'ng', retrodeck: 'neogeo', + romm: 'neogeomvs', }), new GameConsole(/Neo ?Geo CD/i, [/* '.bin', '.cue' */], { batocera: 'neogeocd', @@ -860,6 +944,7 @@ export default class GameConsole { jelos: 'neocd', onion: 'NEOCD', retrodeck: 'neogeocd', + romm: 'neo-geo-cd', }), new GameConsole(/Neo ?Geo Pocket/i, ['.ngp'], { adam: 'NGP', @@ -870,6 +955,7 @@ export default class GameConsole { minui: 'Neo Geo Pocket (NGPC)', // added for sorting convenience onion: 'NGP', retrodeck: 'ngp', + romm: 'neo-geo-pocket', twmenu: 'ngp', }), new GameConsole(/Neo ?Geo Pocket Color/i, ['.ngc', '.ngpc', '.npc'], { @@ -881,6 +967,7 @@ export default class GameConsole { minui: 'Neo Geo Pocket Color (NGPC)', // added for sorting convenience onion: 'NGP', retrodeck: 'ngpc', + romm: 'neo-geo-pocket-color', twmenu: 'ngp', }), // Sony @@ -895,29 +982,34 @@ export default class GameConsole { miyoocfw: 'PS1', onion: 'PS', retrodeck: 'psx', + romm: 'ps', }), new GameConsole(/PlayStation 2|ps2/i, [/* '.bin', '.cue' */], { batocera: 'ps2', emulationstation: 'ps2', jelos: 'ps2', retrodeck: 'ps2', + romm: 'ps2', }), new GameConsole(/PlayStation 3|ps3/i, ['.ps3', '.ps3dir'], { batocera: 'ps3', emulationstation: 'ps3', jelos: 'ps3', retrodeck: 'ps3', + romm: 'ps3', }), new GameConsole(/PlayStation ?Portable|psp/i, ['.cso'], { batocera: 'psp', emulationstation: 'psp', jelos: 'psp', retrodeck: 'psp', + romm: 'psp', }), new GameConsole(/PlayStation ?Vita|psvita/i, ['.psvita'], { batocera: 'psvita', emulationstation: 'psvita', retrodeck: 'psvita', + romm: 'psvita', }), new GameConsole(/PlayStation [4-9]|ps[4-9]/i, [/* '.bin' */], {}), // Sord @@ -928,11 +1020,13 @@ export default class GameConsole { new GameConsole(/TI-?99-?4A/i, ['.rpk'], { emulationstation: 'ti99', retrodeck: 'ti99', + romm: 'ti-99', }), // Tiger new GameConsole(/Game.?com/i, ['.tgc'], { emulationstation: 'gamecom', retrodeck: 'gamecom', + romm: 'game-dot-com', }), // Timetop new GameConsole(/GameKing/i, [/* '.bin' */], { @@ -950,6 +1044,7 @@ export default class GameConsole { batocera: 'vsmile', emulationstation: 'vsmile', retrodeck: 'vsmile', + romm: 'vsmile', }), // Watara new GameConsole(/Supervision/i, ['.sv'], { @@ -961,6 +1056,7 @@ export default class GameConsole { onion: 'SUPERVISION', pocket: 'supervision', retrodeck: 'supervision', + romm: 'watara-slash-quickshot-supervision', }), // Wellback new GameConsole(/Mega Duck/i, ['.md1', '.md2'], { @@ -970,6 +1066,7 @@ export default class GameConsole { onion: 'MEGADUCK', pocket: 'mega_duck', retrodeck: 'megaduck', + romm: 'mega-duck-slash-cougar-boy', }), ]; @@ -1053,6 +1150,10 @@ export default class GameConsole { return this.outputTokens.retrodeck; } + getRomM(): string | undefined { + return this.outputTokens.romm; + } + getTWMenu(): string | undefined { return this.outputTokens.twmenu; } diff --git a/src/types/outputFactory.ts b/src/types/outputFactory.ts index 654602294..02746f006 100644 --- a/src/types/outputFactory.ts +++ b/src/types/outputFactory.ts @@ -315,6 +315,11 @@ export default class OutputFactory { output = output.replace('{retrodeck}', retrodeck); } + const romm = gameConsole.getRomM(); + if (romm) { + output = output.replace('{romm}', romm); + } + const twmenu = gameConsole.getTWMenu(); if (twmenu) { output = output.replace('{twmenu}', twmenu); diff --git a/test/outputFactory.test.ts b/test/outputFactory.test.ts index 6d92e7304..14adc57c7 100644 --- a/test/outputFactory.test.ts +++ b/test/outputFactory.test.ts @@ -736,6 +736,48 @@ describe('token replacement', () => { }, ); + // Output Token {romm} + test.each([ + ['game.d88', path.join('roms', 'pc-8800-series', 'game.d88')], + ['game.gb', path.join('roms', 'gb', 'game.gb')], + ['game.nes', path.join('roms', 'nes', 'game.nes')], + ['game.pqa', path.join('roms', 'palm-os', 'game.pqa')], + ])( + 'should replace {romm} for known extension: %s', + async (outputRomFilename, expectedPath) => { + const options = new Options({ commands: ['copy'], output: 'roms/{romm}' }); + const rom = new ROM({ name: outputRomFilename, size: 0, crc32: '' }); + + const outputPath = OutputFactory.getPath( + options, + dummyDat, + dummyGame, + dummyRelease, + rom, + await rom.toFile(), + ); + expect(outputPath.format()).toEqual(expectedPath); + }, + ); + + test.each(['game.bin', 'game.rom'])( + 'should throw on {romm} for unknown extension: %s', + async (outputRomFilename) => { + const options = new Options({ commands: ['copy'], output: 'roms/{romm}' }); + + const rom = new ROM({ name: outputRomFilename, size: 0, crc32: '' }); + + await expect(async () => OutputFactory.getPath( + options, + dummyDat, + dummyGame, + dummyRelease, + rom, + await rom.toFile(), + )).rejects.toThrow(/failed to replace/); + }, + ); + // Output Token {twmenu} test.each([ ['game.a26', path.join('roms', 'a26', 'game.a26')], diff --git a/test/types/cache.test.ts b/test/types/cache.test.ts index 38002dd53..bebb4ab6b 100644 --- a/test/types/cache.test.ts +++ b/test/types/cache.test.ts @@ -174,14 +174,14 @@ describe('load', () => { await expect(cache.load()).resolves.toBeTruthy(); }); - it('should throw on empty file', async () => { + it('should not throw on empty file', async () => { const tempFile = await FsPoly.mktemp(path.join(Constants.GLOBAL_TEMP_DIR, 'cache')); await FsPoly.touch(tempFile); try { await expect(FsPoly.exists(tempFile)).resolves.toEqual(true); const cache = new Cache({ filePath: tempFile }); - await expect(cache.load()).rejects.toThrow(); + await expect(cache.load()).resolves.toBeTruthy(); } finally { await FsPoly.rm(tempFile, { force: true }); }