diff --git a/docs/CLIHELP.md b/docs/CLIHELP.md index b059ae4d..28379a4f 100644 --- a/docs/CLIHELP.md +++ b/docs/CLIHELP.md @@ -84,12 +84,14 @@ This sets the extra command (or emulator command) for some frontends. !!! info - This option is applicable _only_ when using the `-f attractmode` or the `-f pegasus` option. + This option is applicable _only_ when using the `-f attractmode`, the `-f pegasus` or the `-f retroarch` option. When using `-f attractmode` it is **required** to set the _emulator_ to be used when generating the `attractmode` game list. On RetroPie the emulator name is mostly the same as the platform. Consider setting this in [`config.ini`](CONFIGINI.md#emulator) instead. The extra command can **optionally** be used with `-f pegasus` to set the launch command used by the Pegasus game list. On RetroPie this defaults to the RetroPie launch command which works with RetroPie, if this parameter is unset. Consider setting this in [`config.ini`](CONFIGINI.md#launch) instead. +The extra command can **optionally** be used with `-f retroarch` with `-e ";"` to set a default core (path,name) pair for the playlist. Consider setting this in [`config.ini`](CONFIGINI.md#emulator) instead. + **Example(s)** ``` diff --git a/docs/CONFIGINI.md b/docs/CONFIGINI.md index 65ed1f01..44f9c475 100644 --- a/docs/CONFIGINI.md +++ b/docs/CONFIGINI.md @@ -121,6 +121,7 @@ This is an alphabetical index of all configuration options their usage level and | [onlyMissing](CONFIGINI.md#onlymissing) | Advanced | Y | Y | | Y | | [platform](CONFIGINI.md#platform) | Basic | Y | | | | | [pretend](CONFIGINI.md#pretend) | Basic | Y | Y | | | +| [raExtra](CONFIGINI.md#raextra) | Advanced | | Y | Y | | | [region](CONFIGINI.md#region) | Basic | Y | Y | | | | [regionPrios](CONFIGINI.md#regionprios) | Expert | Y | Y | | | | [relativePaths](CONFIGINI.md#relativepaths) | Basic | Y | Y | | | @@ -460,6 +461,15 @@ Allowed in sections: `[main]`, `[]`, `[]` --- +#### raExtra + +This option is _only_ applicable when also setting the `frontend="retroarch"` option. With this setting you can define a default core (path,name) pair for the playlist. + +Default value: unset +Allowed in sections: `[]`, `[retroarch]` + +--- + #### videos By default Skyscraper does not scrape and cache video resources because of the significant disk space required to save them. You can enable videos using this option. If your frontend supports video display also explicitly set this option to true. See also the option to [symlink video files](#symlink) instead of copying, if space is a premium. diff --git a/docs/FRONTENDS.md b/docs/FRONTENDS.md index ffc9c92f..4c990dfd 100644 --- a/docs/FRONTENDS.md +++ b/docs/FRONTENDS.md @@ -28,7 +28,7 @@ Skyscraper will preserve the following metadata when re-generating a game list f Automatic addition of folder elements if [`addFolder`](CONFIGINI.md#addfolders) is true: If at least one ROM is within a subfolder and this subfolder is not yet part of the `gamelist.xml` file, it will be added with two mandatory subelements: -- `` reflects the relative subpath from the system folder and +- `` reflects the relative subpath from the system folder and - ``, which represents the direct parent folder of a ROM by default. However, you may edit this to any name which should be shown in EmulationStation. !!! example @@ -288,4 +288,82 @@ You need to add the individual platform rom directories to Pegasus (if they are #### Metadata preservation -Skyscraper will preserve any metadata key-value pairs added to the header and / or individual game list entries. \ No newline at end of file +Skyscraper will preserve any metadata key-value pairs added to the header and / or individual game list entries. + +### RetroArch + +- Default game list location: `~/.config/retroarch/playlists` +- Default game list filename: `.lpl` (using its own platform name format, defined in Skyscraper's `peas.json`) +- Default media dir location: `~/.config/retroarch/thumbnails//Named_*` (see table below) + +RetroArch structuring element on the GUI is build around playlists. Each +playlist contains each platform's game list, and the playlist viewer suports +covers, screenshots, and logos for each game. + +#### Configuration + +When generating for RetroArch, Skyscraper uses a configurable mapping of +platform names to RetroArch database names (e.g., `nes` (folder) → `Nintendo - +Nintendo Entertainment System` (core name), see file `~/.skyscraper/peas.json`). +If you want to add a platform to RetroArch DB-Name mapping, please file an +issue. That way it will be of use for every Skyscraper user. Any setting +specific to your setup you can define in `~/.skyscraper/peas_local.json`. This +file uses the same format as the `peas.json`. + +You can optionally use the `-e` parameter with `";"` to +set a default core path/name for the playlist. Or set +[`raExtra`](CONFIGINI.md#raextra) it in `config.ini` like: + +```ini +; also allowed in [] +[retroarch] +raExtra=";" +``` + +You will most likely have to adjust the game list folder (`gameListFolder=`) and +the media files folder (`mediaFolder=`) by persisting them in your `config.ini` +in the `[retroarch]` section. Also you may want to set the +`artworkXml=retroarch-artwork.xml` in your configuration to assure every media +file is in PNG format. + +#### Media Support + +RetroArch supports the following media types: + +| Media Type | RetroArch Directory | +| :---------------------- | :------------------ | +| Covers (Box Art) | `Named_Boxarts` | +| Screenshots | `Named_Snaps` | +| Marquees/Wheels (Logos) | `Named_Logos` | + +Title screenshots (`Named_Titles`) are not currently supported. +All media files are matched to games by their [sanitized game +title](https://docs.libretro.com/guides/roms-playlists-thumbnails/#custom-thumbnails), +not by ROM filename. + +#### Metadata preservation + +Skyscraper will preserve existing game titles and paths when re-generating a +game list for RetroArch. If an existing playlist file is found and you choose to +skip existing entries, Skyscraper will use the old game list as a reference. + +#### Known Limitations + +1. RetroArch's [deprecated playlist + format](https://docs.libretro.com/guides/roms-playlists-thumbnails/#6-line-playlist-format-deprecated) + with plain six lines per game is not supported. +2. Initial generation of a RetroArch playlist does not work for folders resp. + platforms which may require a different RetroArch core per game (for example + `arcade/` on RetroPie). However, updating an existing playlist file for such + folder works, as the existing entry of `"db_name"` per each game is + preserved. +3. Any RetroArch configuration is not evaluated, thus if you changed your + RetroArch configuration (e.g., by using non-default file paths) the produced + JSON/lpl file might not display correctly in RetroArch frontend. +4. Existing compressed playlists are not supported yet and will not be read by + Skyscraper to preserver any data, thus you may lose previous changes. Before + you start using Skyscraper's RetroArch playlist output you should disable the + playlist compression in RetroArch and save the playlist from RetroArch as + plain JSON. +5. The settings of "scan_content_dir" in an existing playlist are not yet + preserved. \ No newline at end of file diff --git a/docs/PATHHANDLING.md b/docs/PATHHANDLING.md index e8eec271..5ae36d33 100644 --- a/docs/PATHHANDLING.md +++ b/docs/PATHHANDLING.md @@ -12,7 +12,11 @@ how the absolute path is calculated when a you provide a relative path. Do not get confused by the lenghty flow diagram below. It covers game list folder, input folder and media folder handling. You wiil notice that input folder and -media folder are processed in the same manner. +media folder are almost processed in the same manner. + +If you use generate output with Skyscraper for RetroArch as frontend you can +stop reading as Skyscraper expects any of the three parameters input, gamelist +and media folder to be absolute.
diff --git a/peas.json b/peas.json index 90f68030..bdbeed0f 100644 --- a/peas.json +++ b/peas.json @@ -5,7 +5,8 @@ "*.chd", "*.cue", "*.iso" - ] + ], + "retroarch_dbname": "The 3DO Company - 3DO" }, "3ds": { "aliases": [ @@ -15,7 +16,8 @@ "formats": [ "*.3ds", "*.cci" - ] + ], + "retroarch_dbname": "Nintendo - Nintendo 3DS" }, "actionmax": { "aliases": [ @@ -61,7 +63,8 @@ "*.lha", "*.rp9", "*.uae" - ] + ], + "retroarch_dbname": "Commodore - Amiga" }, "amstradcpc": { "aliases": [ @@ -74,7 +77,8 @@ "*.cpr", "*.dsk", "*.tap" - ] + ], + "retroarch_dbname": "Amstrad - CPC" }, "apple2": { "aliases": [ @@ -169,7 +173,8 @@ "*.dat", "*.fba", "*.iso" - ] + ], + "retroarch_dbname": "FBNeo - Arcade Games" }, "arcadia": { "aliases": [ @@ -178,7 +183,8 @@ ], "formats": [ "*.bin" - ] + ], + "retroarch_dbname": "Emerson - Arcadia 2001" }, "arduboy": { "aliases": [ @@ -186,7 +192,8 @@ ], "formats": [ "*.hex" - ] + ], + "retroarch_dbname": "Arduboy Inc - Arduboy" }, "astrocade": { "aliases": [ @@ -204,7 +211,8 @@ "*.bin", "*.gz", "*.rom" - ] + ], + "retroarch_dbname": "Atari - 2600" }, "atari5200": { "aliases": [ @@ -221,7 +229,8 @@ "*.xex", "*.xfd", "*.xfd.gz" - ] + ], + "retroarch_dbname": "Atari - 5200" }, "atari7800": { "aliases": [ @@ -230,7 +239,8 @@ "formats": [ "*.a78", "*.bin" - ] + ], + "retroarch_dbname": "Atari - 7800" }, "atari800": { "aliases": [ @@ -253,7 +263,8 @@ "*.xex", "*.xfd", "*.xfd.gz" - ] + ], + "retroarch_dbname": "Atari - 8-bit" }, "atarijaguar": { "aliases": [ @@ -263,7 +274,8 @@ "formats": [ "*.j64", "*.jag" - ] + ], + "retroarch_dbname": "Atari - Jaguar" }, "atarijaguarcd": { "aliases": [ @@ -286,7 +298,8 @@ ], "formats": [ "*.lnx" - ] + ], + "retroarch_dbname": "Atari - Lynx" }, "atarist": { "aliases": [ @@ -301,7 +314,8 @@ "*.rom", "*.st", "*.stx" - ] + ], + "retroarch_dbname": "Atari - ST" }, "atomiswave": { "aliases": [ @@ -364,7 +378,8 @@ "*.bin", "*.chd", "*.dat" - ] + ], + "retroarch_dbname": "Atomiswave" }, "bbcmicro": { "aliases": [ @@ -419,7 +434,8 @@ "*.tap", "*.vsf", "*.x64" - ] + ], + "retroarch_dbname": "Commodore - 64" }, "cd32": { "aliases": [ @@ -438,7 +454,8 @@ "*.lha", "*.rp9", "*.uae" - ] + ], + "retroarch_dbname": "Commodore - CD32" }, "cdi": { "aliases": [ @@ -448,7 +465,8 @@ ], "formats": [ "*.chd" - ] + ], + "retroarch_dbname": "Philips - CD-i" }, "cdtv": { "aliases": [ @@ -466,7 +484,8 @@ "*.lha", "*.rp9", "*.uae" - ] + ], + "retroarch_dbname": "Commodore - CDTV" }, "channelf": { "aliases": [ @@ -476,7 +495,8 @@ "formats": [ "*.bin", "*.rom" - ] + ], + "retroarch_dbname": "Fairchild - Channel F" }, "coco": { "aliases": [ @@ -507,7 +527,8 @@ "*.bin", "*.col", "*.rom" - ] + ], + "retroarch_dbname": "Coleco - ColecoVision" }, "crvision": { "aliases": [ @@ -515,7 +536,8 @@ ], "formats": [ "*.rom" - ] + ], + "retroarch_dbname": "VTech - CreatiVision" }, "daphne": { "aliases": [ @@ -558,7 +580,8 @@ "*.cue", "*.gdi", "*.iso" - ] + ], + "retroarch_dbname": "Sega - Dreamcast" }, "easyrpg": { "aliases": [], @@ -640,7 +663,8 @@ "*.cue", "*.fba", "*.iso" - ] + ], + "retroarch_dbname": "FBNeo - Arcade Games" }, "fds": { "aliases": [ @@ -653,7 +677,8 @@ "formats": [ "*.fds", "*.nes" - ] + ], + "retroarch_dbname": "Nintendo - Family Computer Disk System" }, "fm7": { "aliases": [ @@ -708,7 +733,8 @@ ], "formats": [ "*.tgc" - ] + ], + "retroarch_dbname": "Tiger - Game.com" }, "gamegear": { "aliases": [ @@ -719,7 +745,8 @@ "*.bin", "*.gg", "*.sms" - ] + ], + "retroarch_dbname": "Sega - Game Gear" }, "gb": { "aliases": [ @@ -728,7 +755,8 @@ ], "formats": [ "*.gb" - ] + ], + "retroarch_dbname": "Nintendo - Game Boy" }, "gba": { "aliases": [ @@ -737,7 +765,8 @@ ], "formats": [ "*.gba" - ] + ], + "retroarch_dbname": "Nintendo - Game Boy Advance" }, "gbc": { "aliases": [ @@ -747,7 +776,8 @@ ], "formats": [ "*.gbc" - ] + ], + "retroarch_dbname": "Nintendo - Game Boy Color" }, "gc": { "aliases": [ @@ -761,7 +791,8 @@ "*.gcz", "*.iso", "*.rvz" - ] + ], + "retroarch_dbname": "Nintendo - GameCube" }, "gmaster": { "aliases": [ @@ -770,7 +801,8 @@ ], "formats": [ "*.bin" - ] + ], + "retroarch_dbname": "Hartung - Game Master" }, "intellivision": { "aliases": [], @@ -779,7 +811,8 @@ "*.int", "*.itv", "*.rom" - ] + ], + "retroarch_dbname": "Mattel - Intellivision" }, "j2me": { "aliases": [ @@ -876,7 +909,8 @@ "video system co.", "visco" ], - "formats": [] + "formats": [], + "retroarch_dbname": "MAME" }, "mame-advmame": { "aliases": [ @@ -948,7 +982,8 @@ "video system co.", "visco" ], - "formats": [] + "formats": [], + "retroarch_dbname": "MAME" }, "mame-libretro": { "aliases": [ @@ -1020,7 +1055,8 @@ "video system co.", "visco" ], - "formats": [] + "formats": [], + "retroarch_dbname": "MAME" }, "mame-mame4all": { "aliases": [ @@ -1092,7 +1128,8 @@ "video system co.", "visco" ], - "formats": [] + "formats": [], + "retroarch_dbname": "MAME" }, "mastersystem": { "aliases": [ @@ -1102,7 +1139,8 @@ "formats": [ "*.bin", "*.sms" - ] + ], + "retroarch_dbname": "Sega - Master System - Mark III" }, "megadrive": { "aliases": [ @@ -1118,7 +1156,8 @@ "*.md", "*.sg", "*.smd" - ] + ], + "retroarch_dbname": "Sega - Mega Drive - Genesis" }, "megaduck": { "aliases": [ @@ -1144,7 +1183,8 @@ "*.m7", "*.rom", "*.sap" - ] + ], + "retroarch_dbname": "Thomson - MOTO" }, "msx": { "aliases": [ @@ -1159,7 +1199,8 @@ "*.mx1", "*.mx2", "*.rom" - ] + ], + "retroarch_dbname": "Microsoft - MSX" }, "msx2": { "aliases": [ @@ -1174,7 +1215,8 @@ "*.mx1", "*.mx2", "*.rom" - ] + ], + "retroarch_dbname": "Microsoft - MSX2" }, "n64": { "aliases": [ @@ -1184,7 +1226,8 @@ "*.n64", "*.v64", "*.z64" - ] + ], + "retroarch_dbname": "Nintendo - Nintendo 64" }, "n64dd": { "aliases": [ @@ -1195,7 +1238,8 @@ "*.ndd", "*.v64", "*.z64" - ] + ], + "retroarch_dbname": "Nintendo - Nintendo 64DD" }, "naomi": { "aliases": [ @@ -1269,7 +1313,8 @@ "*.bin", "*.chd", "*.dat" - ] + ], + "retroarch_dbname": "Sega - Naomi" }, "naomi2": { "aliases": [ @@ -1289,7 +1334,8 @@ ], "formats": [ "*.nds" - ] + ], + "retroarch_dbname": "Nintendo - Nintendo DS" }, "neogeo": { "aliases": [ @@ -1306,7 +1352,8 @@ "*.cue", "*.fba", "*.iso" - ] + ], + "retroarch_dbname": "SNK - Neo Geo" }, "neogeocd": { "aliases": [ @@ -1322,7 +1369,8 @@ "*.chd", "*.cue", "*.iso" - ] + ], + "retroarch_dbname": "SNK - Neo Geo CD" }, "nes": { "aliases": [ @@ -1337,7 +1385,8 @@ "*.sfc", "*.smc", "*.swc" - ] + ], + "retroarch_dbname": "Nintendo - Nintendo Entertainment System" }, "ngp": { "aliases": [ @@ -1346,7 +1395,8 @@ ], "formats": [ "*.ngp" - ] + ], + "retroarch_dbname": "SNK - Neo Geo Pocket" }, "ngpc": { "aliases": [ @@ -1355,7 +1405,8 @@ ], "formats": [ "*.ngc" - ] + ], + "retroarch_dbname": "SNK - Neo Geo Pocket Color" }, "openbor": { "aliases": [], @@ -1409,7 +1460,8 @@ "*.m3u8", "*.sh", "*.vhd" - ] + ], + "retroarch_dbname": "DOS" }, "pc88": { "aliases": [ @@ -1421,7 +1473,8 @@ "*.cmt", "*.d88", "*.t88" - ] + ], + "retroarch_dbname": "NEC - PC-8001 - PC-8801" }, "pc98": { "aliases": [ @@ -1446,7 +1499,8 @@ "*.tfd", "*.thd", "*.xdf" - ] + ], + "retroarch_dbname": "NEC - PC-98" }, "pcengine": { "aliases": [ @@ -1464,7 +1518,8 @@ "*.chd", "*.cue", "*.pce" - ] + ], + "retroarch_dbname": "NEC - PC Engine - TurboGrafx 16" }, "pcenginecd": { "aliases": [ @@ -1482,7 +1537,8 @@ "*.chd", "*.cue", "*.pce" - ] + ], + "retroarch_dbname": "NEC - PC Engine CD - TurboGrafx-CD" }, "pcfx": { "aliases": [ @@ -1495,7 +1551,8 @@ "*.img", "*.iso", "*.pce" - ] + ], + "retroarch_dbname": "NEC - PC-FX" }, "pico8": { "aliases": [ @@ -1523,7 +1580,8 @@ "*.tap", "*.vsf", "*.x64" - ] + ], + "retroarch_dbname": "Commodore - Plus-4" }, "pokemini": { "aliases": [ @@ -1532,7 +1590,8 @@ ], "formats": [ "*.min" - ] + ], + "retroarch_dbname": "Nintendo - Pokemon Mini" }, "ports": { "aliases": [ @@ -1570,7 +1629,8 @@ "*.mdf", "*.z", "*.z2" - ] + ], + "retroarch_dbname": "Sony - PlayStation 2" }, "ps3": { "aliases": [ @@ -1581,7 +1641,8 @@ "*.bin", "*.iso", "*.pkg" - ] + ], + "retroarch_dbname": "Sony - PlayStation 3" }, "ps4": { "aliases": [ @@ -1591,7 +1652,8 @@ "formats": [ "*.bin", "*.iso" - ] + ], + "retroarch_dbname": "Sony - PlayStation 4" }, "ps5": { "aliases": [ @@ -1613,7 +1675,8 @@ "*.cso", "*.iso", "*.pbp" - ] + ], + "retroarch_dbname": "Sony - PlayStation Portable" }, "psvita": { "aliases": [ @@ -1622,7 +1685,8 @@ ], "formats": [ "*.psvita" - ] + ], + "retroarch_dbname": "Sony - PlayStation Vita" }, "psx": { "aliases": [ @@ -1640,7 +1704,8 @@ "*.toc", "*.z", "*.znx" - ] + ], + "retroarch_dbname": "Sony - PlayStation" }, "pv1000": { "aliases": [ @@ -1649,7 +1714,8 @@ ], "formats": [ "*.bin" - ] + ], + "retroarch_dbname": "Casio - PV-1000" }, "samcoupe": { "aliases": [ @@ -1672,7 +1738,8 @@ "*.cue", "*.iso", "*.mdf" - ] + ], + "retroarch_dbname": "Sega - Saturn" }, "scummvm": { "aliases": [ @@ -1690,7 +1757,8 @@ "formats": [ "*.scummvm", "*.svm" - ] + ], + "retroarch_dbname": "ScummVM" }, "scv": { "aliases": [ @@ -1699,7 +1767,8 @@ "formats": [ "*.0", "*.bin" - ] + ], + "retroarch_dbname": "Epoch - Super Cassette Vision" }, "sega32x": { "aliases": [ @@ -1711,7 +1780,8 @@ "*.bin", "*.md", "*.smd" - ] + ], + "retroarch_dbname": "Sega - 32X" }, "segacd": { "aliases": [ @@ -1723,7 +1793,8 @@ "*.chd", "*.cue", "*.iso" - ] + ], + "retroarch_dbname": "Sega - Mega-CD - Sega CD" }, "sg-1000": { "aliases": [ @@ -1732,7 +1803,8 @@ "formats": [ "*.bin", "*.sg" - ] + ], + "retroarch_dbname": "Sega - SG-1000" }, "snes": { "aliases": [ @@ -1754,7 +1826,8 @@ "*.sfc", "*.smc", "*.swc" - ] + ], + "retroarch_dbname": "Nintendo - Super Nintendo Entertainment System" }, "solarus": { "aliases": [], @@ -1790,7 +1863,8 @@ "formats": [ "*.bin", "*.sv" - ] + ], + "retroarch_dbname": "Watara - Supervision" }, "switch": { "aliases": [ @@ -1828,7 +1902,8 @@ ], "formats": [ "*.tic" - ] + ], + "retroarch_dbname": "TIC-80" }, "trs-80": { "aliases": [ @@ -1844,7 +1919,8 @@ "*.bin", "*.gam", "*.vec" - ] + ], + "retroarch_dbname": "GCE - Vectrex" }, "vic20": { "aliases": [ @@ -1860,7 +1936,8 @@ "*.tap", "*.vsf", "*.x64" - ] + ], + "retroarch_dbname": "Commodore - VIC-20" }, "videopac": { "aliases": [ @@ -1873,13 +1950,15 @@ ], "formats": [ "*.bin" - ] + ], + "retroarch_dbname": "Magnavox - Odyssey2" }, "vircon32": { "aliases": [], "formats": [ "*.v32" - ] + ], + "retroarch_dbname": "Vircon32" }, "virtualboy": { "aliases": [ @@ -1888,7 +1967,8 @@ ], "formats": [ "*.vb" - ] + ], + "retroarch_dbname": "Nintendo - Virtual Boy" }, "vsmile": { "aliases": [ @@ -1896,7 +1976,8 @@ ], "formats": [ "*.bin" - ] + ], + "retroarch_dbname": "VTech - V.Smile" }, "wii": { "aliases": [ @@ -1912,7 +1993,8 @@ "*.rvz", "*.wad", "*.wbfs" - ] + ], + "retroarch_dbname": "Nintendo - Wii" }, "wiiu": { "aliases": [ @@ -1930,13 +2012,15 @@ "*.wua", "*.wud", "*.wux" - ] + ], + "retroarch_dbname": "Nintendo - Wii U" }, "wonderswan": { "aliases": [], "formats": [ "*.ws" - ] + ], + "retroarch_dbname": "Bandai - WonderSwan" }, "wonderswancolor": { "aliases": [ @@ -1944,7 +2028,8 @@ ], "formats": [ "*.wsc" - ] + ], + "retroarch_dbname": "Bandai - WonderSwan Color" }, "x1": { "aliases": [], @@ -1959,7 +2044,8 @@ "*.hdm", "*.tfd", "*.xdf" - ] + ], + "retroarch_dbname": "Sharp - X1" }, "x68000": { "aliases": [ @@ -1972,7 +2058,8 @@ "*.hdf", "*.hdm", "*.xdf" - ] + ], + "retroarch_dbname": "Sharp - X68000" }, "xbox": { "aliases": [ @@ -1981,13 +2068,15 @@ "formats": [ "*.iso", "*.xbe" - ] + ], + "retroarch_dbname": "Microsoft - Xbox" }, "xbox360": { "aliases": [], "formats": [ "*.iso" - ] + ], + "retroarch_dbname": "Microsoft - Xbox 360" }, "zmachine": { "aliases": [ @@ -2012,7 +2101,8 @@ "*.p", "*.t81", "*.tzx" - ] + ], + "retroarch_dbname": "Sinclair - ZX 81" }, "zxspectrum": { "aliases": [ @@ -2033,6 +2123,7 @@ "*.tzx", "*.udi", "*.z80" - ] + ], + "retroarch_dbname": "Sinclair - ZX Spectrum" } } \ No newline at end of file diff --git a/platforms_idmap.csv b/platforms_idmap.csv index a5d3b482..be1f5e4e 100644 --- a/platforms_idmap.csv +++ b/platforms_idmap.csv @@ -1,4 +1,4 @@ -folder,screenscraper_id,mobygames_id,tgdb_id +folder,screenscraper_id,mobygames_id,tgdb_id,retroarch_dbname # Mapping between platform folder and # - screenscraper system id @@ -8,11 +8,11 @@ folder,screenscraper_id,mobygames_id,tgdb_id # If you need more than one match use the alias property in platforms.json. # # Use supplementary/scraperdata/peas_and_idmap_verify.py to view platform names -# of the ids below. +# of the ids below. ### Begin RetroPie OOTB supported systems -# platform/folder,screenscraper_id,moby_id,tgdb_id +# platform_folder,screenscraper_id,moby_id,tgdb_id 3do,29,35,25 ags,138,3,1 amiga,64,19,4911 @@ -53,7 +53,7 @@ mame-libretro,75,143,23 mame-mame4all,75,143,23 mastersystem,2,26,35 megadrive,1,16,36|18 -moto,141,147,-1 +moto,141,130|147,5022|5023 msx,113,57,4929 n64,14,9,3 nds,15,44,8 @@ -103,7 +103,7 @@ zxspectrum,76,41,4913 3ds,17,101,4912 actionmax,81,-1,4976 -apple2gs,217,-1,-1 +apple2gs,217,51,-1 arduboy,263,215,-1 astrocade,44,160,4968 atarijaguarcd,171,-1,29 @@ -127,6 +127,7 @@ pcenginecd,114,45,4955 plus4,99,115,5007|5006 ps3,59,81,12 ps4,60,141,4919 +ps5,284,288,4980 psvita,62,105,39 pv1000,74,125,4964 scv,67,138,4966 diff --git a/retroarch-artwork.xml b/retroarch-artwork.xml new file mode 100644 index 00000000..150e97cb --- /dev/null +++ b/retroarch-artwork.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/skyscraper.pro b/skyscraper.pro index f6b6e07f..07d9c6f7 100644 --- a/skyscraper.pro +++ b/skyscraper.pro @@ -54,7 +54,8 @@ unix:config.files=aliasMap.csv hints.xml mameMap.csv \ unix:examples.path=$${SYSCONFDIR}/skyscraper unix:examples.files=config.ini.example README.md artwork.xml \ artwork.xml.example1 artwork.xml.example2 artwork.xml.example3 \ - artwork.xml.example4 batocera-artwork.xml docs/ARTWORK.md docs/CACHE.md + artwork.xml.example4 batocera-artwork.xml retroarch-artwork.xml \ + docs/ARTWORK.md docs/CACHE.md unix:cacheexamples.path=$${SYSCONFDIR}/skyscraper/cache unix:cacheexamples.files=cache/priorities.xml.example docs/CACHE.md @@ -132,6 +133,7 @@ HEADERS += \ src/pegasus.h \ src/platform.h \ src/queue.h \ + src/retroarch.h \ src/scraperworker.h \ src/screenscraper.h \ src/settings.h \ @@ -188,6 +190,7 @@ SOURCES += src/main.cpp \ src/pegasus.cpp \ src/platform.cpp \ src/queue.cpp \ + src/retroarch.cpp \ src/scraperworker.cpp \ src/screenscraper.cpp \ src/settings.cpp \ diff --git a/src/abstractfrontend.cpp b/src/abstractfrontend.cpp index ff9d0025..6f376c05 100644 --- a/src/abstractfrontend.cpp +++ b/src/abstractfrontend.cpp @@ -336,4 +336,4 @@ bool AbstractFrontend::doCopy(GameEntry::Types t, const QString &cacheFn, qDebug() << "Copied" << t; } return success; -} \ No newline at end of file +} diff --git a/src/cli.cpp b/src/cli.cpp index 92e1c613..cc554fba 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -85,8 +85,8 @@ void Cli::createParser(QCommandLineParser *parser, QString platforms) { "The frontend you wish to generate a gamelist for. Remember to leave " "out the '-s' option when using this in order to enable Skyscraper's " "gamelist generation mode.\nCurrently supports 'emulationstation', " - "'esde', 'batocera', 'retrobat', 'attractmode' and 'pegasus'. Default: " - "'emulationstation'", + "'esde', 'batocera', 'retrobat', 'attractmode', 'pegasus' and " + "'retroarch'. Default: 'emulationstation'", "FRONTEND", ""); QCommandLineOption eOption( "e", diff --git a/src/config.cpp b/src/config.cpp index d6343213..7f87410f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -229,6 +229,7 @@ void Config::setupUserConfig() { {"aliasMap.csv", QPair("", FileOp::CREATE_DIST)}, {"artwork.xml", QPair("", FileOp::CREATE_DIST)}, {"batocera-artwork.xml", QPair("", FileOp::CREATE_DIST)}, + {"retroarch-artwork.xml", QPair("", FileOp::CREATE_DIST)}, {"peas.json", QPair("", FileOp::CREATE_DIST)}, {"platforms_idmap.csv", QPair("", FileOp::CREATE_DIST)} // clang-format on diff --git a/src/gameentry.h b/src/gameentry.h index 6dc07a26..f41b3436 100644 --- a/src/gameentry.h +++ b/src/gameentry.h @@ -60,7 +60,7 @@ class GameEntry { }; Q_DECLARE_FLAGS(Types, Elem) - enum Format { RETROPIE, ESDE, BATOCERA, ATTRACT, PEGASUS }; + enum Format { RETROPIE, ESDE, BATOCERA, ATTRACT, PEGASUS, RETROARCH }; static constexpr GameEntry::Types MEDIA = GameEntry::Types(BACKCOVER | COVER | FANART | MANUAL | MARQUEE | diff --git a/src/platform.cpp b/src/platform.cpp index 58fe1a3d..61abdd73 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -218,8 +218,8 @@ bool Platform::parsePlatformsIdCsv(const QString &platformsIdCsvFn) { } QStringList parts = line.split(','); if (parts.length() != 4) { - ncprintf("\033[1;31mFile '%s', line '%s' has not four columns, but " - "%d. Please fix. Now quitting...\033[0m\n", + ncprintf("\033[1;31mFile '%s', line '%s' has not four columns, " + "but %d. Please fix. Now quitting...\033[0m\n", fn, parts.join(',').toUtf8().constData(), static_cast(parts.length())); configFile.close(); @@ -292,7 +292,8 @@ int Platform::isPlatformCfgfilePristine(const QString &cfgFilePath) { "f0dff220a6a07cf1272f00f94d5c55f69353cdce786f8dbfef029dbf30a48a7d", "6c648e3577992caef99c73a6e325a7e9580babf7eafc7ecf35eb349f9da594a1", "fcb923fa1b38441a462511b5b842705c284d91f560d5f30c0a45e68d2444facf", - "fceca636224ec01e50e4d2ce47f43e2ab1d603c008f8292bf50808fcf7f708a3"} + "fceca636224ec01e50e4d2ce47f43e2ab1d603c008f8292bf50808fcf7f708a3", + "c658d5f998b600e81a2e2adc1d216bf00868329ae533a7800c681fe5a421cd6e"} ) }, {"platforms_idmap.csv", QStringList( @@ -345,3 +346,8 @@ QVector Platform::getPlatformIdOnScraper(const QString platform, << "and scraper" << scraper; return id; } + +QString Platform::getRetroArchDbName(const QString platform) const { + QString ra_dbname = peas[platform].toHash()["retroarch_dbname"].toString(); + return ra_dbname; +} diff --git a/src/platform.h b/src/platform.h index d61b3d87..624d941d 100644 --- a/src/platform.h +++ b/src/platform.h @@ -48,6 +48,7 @@ class Platform : public QObject { QStringList getAliases(QString platform) const; QVector getPlatformIdOnScraper(const QString platform, const QString scraper) const; + QString getRetroArchDbName(const QString platform) const; private: bool loadPlatformsIdMap(); diff --git a/src/retroarch.cpp b/src/retroarch.cpp new file mode 100644 index 00000000..556076cd --- /dev/null +++ b/src/retroarch.cpp @@ -0,0 +1,307 @@ +/* + * This file is part of skyscraper. + * Copyright 2026 SineSwiper @ GitHub + * + * skyscraper is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * skyscraper is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with skyscraper; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +#include "retroarch.h" + +#include "gameentry.h" +#include "platform.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const QString LPL_VERSION_VAL = "1.5"; +static const QString DETECT_VAL = "DETECT"; + +const QString META_VERSION = "version"; +const QString META_DFLT_CORE_PATH = "default_core_path"; +const QString META_DFLT_CORE_NAME = "default_core_name"; + +const QStringList LPL_META_PROPS = { + META_DFLT_CORE_PATH, META_DFLT_CORE_NAME, META_VERSION, + "label_display_mode", "left_display_mode", "right_display_mode", + "thumbnail_match_mode", "sort_mode", "base_content_directory"}; + +const QString ITEMS_ARRAY = "items"; + +const QString ITEM_CORE_NAME = "core_name"; +const QString ITEM_CORE_PATH = "core_path"; +const QString ITEM_CRC = "crc32"; +const QString ITEM_DB_NAME = "db_name"; +const QString ITEM_LABEL = "label"; +const QString ITEM_PATH = "path"; + +RetroArch::RetroArch() {} + +QString RetroArch::sanitizeForFilename(const QString &name) { + QString sanitized = name; + // Replace forbidden characters with underscore + sanitized.replace(QRegularExpression("[&*/:\\\\\"<>?|]"), "_"); + return sanitized; +} + +const QString RetroArch::getPlatformOutputName() { + // Look up the RetroArch db_name from peas.json + QString dbName = Platform::get().getRetroArchDbName(config->platform); + + if (dbName.isEmpty()) { + // Fallback to platform name if not found + qWarning() << "Platform" << config->platform + << "not in RetroArch platform mapping, using platform name " + "as-is"; + dbName = config->platform; + } + return dbName; +} + +bool RetroArch::loadOldGameList(const QString &gameListFileString) { + QJsonDocument doc; + if (QFile gameListFile(gameListFileString); + !gameListFile.open(QIODevice::ReadOnly)) { + return false; + } else { + QByteArray jsonData = gameListFile.readAll(); + gameListFile.close(); + doc = QJsonDocument::fromJson(jsonData); + if (!doc.isObject()) { + return false; + } + } + + existingPlaylist = doc.object(); + QJsonArray items = existingPlaylist.value(ITEMS_ARRAY).toArray(); + + for (const QJsonValue &item : items) { + if (item.isObject()) { + QJsonObject itemObj = item.toObject(); + GameEntry oldEntry; + // always absolute path with Retroarch + // path might be contain backslashes on Windows + oldEntry.path = itemObj.value(ITEM_PATH).toString(); + oldEntry.title = itemObj.value(ITEM_LABEL).toString(); + // remaining properties of an item are held in existingPlaylist + oldEntries.append(oldEntry); + } + } + + return true; +} + +void RetroArch::skipExisting(QList &gameEntries, + QSharedPointer queue) { + gameEntries = oldEntries; + + printf("Resolving missing entries..."); + int dots = 0; + for (auto const &ge : gameEntries) { + dots++; + if (dots % 100 == 0) { + printf("."); + fflush(stdout); + } + QFileInfo current(ge.path); + for (auto qi = queue->begin(), end = queue->end(); qi != end; ++qi) { + if (current.isFile()) { + if (current.fileName() == (*qi).fileName()) { + queue->erase(qi); + break; + } + } else if (current.isDir()) { + if (current.absoluteFilePath() == (*qi).absoluteFilePath()) { + queue->erase(qi); + break; + } + } + } + } +} + +void RetroArch::assembleList(QString &finalOutput, + QList &gameEntries) { + if (gameEntries.isEmpty()) + return; + + // Build a map of baseName -> title for use in getTargetFileName + baseNameToTitle.clear(); + for (const auto &entry : gameEntries) { + baseNameToTitle[entry.baseName] = entry.title; + } + + QJsonObject newPlaylist = createMetaProps(); + + QJsonArray exitsingItems = existingPlaylist.value(ITEMS_ARRAY).toArray(); + QJsonObject eitemObj; + + QJsonArray items; + QString gameFn; + + int dots = -1; + int dotMod = gameEntries.length() * 0.1 + 1; + for (auto const &entry : gameEntries) { + if (++dots % dotMod == 0) { + printf("."); + fflush(stdout); + } + gameFn = QFileInfo(entry.path).fileName(); + // TODO: unpack support for CRC and inter-zip reference + // "path": "/storage/emulated/0/ROMs/virtualboy/Game.zip#Game.vb", + // "crc32": "133E9372|crc", + QString absPath = entry.absoluteFilePath; +#ifdef Q_OS_WIN + absPath = absPath.replace("/", "\\\\"); +#endif + bool hasExisting = false; + QJsonObject itemObj; + for (const QJsonValue &eit : exitsingItems) { + if (eit.isObject()) { + eitemObj = eit.toObject(); + if (eitemObj[ITEM_PATH].toString().endsWith(gameFn)) { + hasExisting = true; + itemObj = eitemObj; + break; + } + } + } + + if (!hasExisting) { + itemObj.insert(ITEM_CORE_PATH, DETECT_VAL); + itemObj.insert(ITEM_CORE_NAME, DETECT_VAL); + itemObj.insert(ITEM_CRC, DETECT_VAL); + itemObj.insert(ITEM_DB_NAME, getGameListFileName()); + } + + itemObj.insert(ITEM_PATH, absPath); + itemObj.insert(ITEM_LABEL, entry.title); + + items.append(itemObj); + } + + newPlaylist.insert(ITEMS_ARRAY, items); + + QJsonDocument doc(newPlaylist); + finalOutput = doc.toJson(QJsonDocument::Indented); +} + +QJsonObject RetroArch::createMetaProps() { + QJsonObject newPlaylist; + newPlaylist.insert(META_VERSION, LPL_VERSION_VAL); + + QString corePathStr = DETECT_VAL; + QString coreNameStr = DETECT_VAL; + + // Parse default_core_path and default_core_name from frontendExtra + // (raExtra= or -e) + if (!config->frontendExtra.isEmpty()) { + QStringList parts = config->frontendExtra.split(";"); + corePathStr = parts[0]; + coreNameStr = parts[1]; + } + + // create or restore meta properties + for (const auto &k : LPL_META_PROPS) { + QString v = existingPlaylist[k].toString(); + if (v.isEmpty()) { + if (k == META_VERSION) + newPlaylist.insert(k, LPL_VERSION_VAL); + else if (k == META_DFLT_CORE_NAME) + newPlaylist.insert(k, coreNameStr); + else if (k == META_DFLT_CORE_PATH) + newPlaylist.insert(k, corePathStr); + else if (k == "base_content_directory") + ; // don't set default "base_content_directory" + else + newPlaylist.insert(k, "0"); + } else { + newPlaylist.insert(k, v); + } + } + return newPlaylist; +} + +QString RetroArch::getTargetFileName(GameEntry::Types t, + const QString &baseName) { + (void)t; + // for media files use sanitized title as filename stem + QString title = baseNameToTitle.value(baseName, baseName); + return sanitizeForFilename(title); +} + +bool RetroArch::canSkip() { return true; } + +QString RetroArch::getGameListFileName() { + return config->gameListFilename.isEmpty() + ? (getPlatformOutputName() % ".lpl") + : config->gameListFilename; +} + +QString RetroArch::getInputFolder() { + return QString(QDir::homePath() % "/RetroPie/roms/" % config->platform); +} + +QString RetroArch::getGameListFolder() { + if (config->gameListFolder.isEmpty()) { + return QDir::homePath() % "/.config/retroarch/playlists"; + } else { + if (config->gameListFolder.endsWith("/" % config->platform)) { + return config->gameListFolder.replace("/" % config->platform, ""); + } + return config->gameListFolder; + } +} + +QString RetroArch::getMediaFolder() { + if (config->mediaFolder.isEmpty()) { + return QDir::homePath() % "/.config/retroarch/thumbnails/" % + getPlatformOutputName(); + } else { + if (config->mediaFolder.endsWith("/" % config->platform)) { + return config->mediaFolder.replace("/" % config->platform, + "/" % getPlatformOutputName()); + } + return config->mediaFolder; + } +} + +QString RetroArch::getCoversFolder() { + return config->mediaFolder % "/Named_Boxarts"; +} + +QString RetroArch::getScreenshotsFolder() { + return config->mediaFolder % "/Named_Snaps"; +} + +QString RetroArch::getMarqueesFolder() { + return config->mediaFolder % "/Named_Logos"; +} + +QString RetroArch::getWheelsFolder() { + return config->mediaFolder % "/Named_Logos"; +} + +// PENDING: This media type is supported by RA but not yet by Skyscraper +/* +QString RetroArch::getTitleScreenshotsFolder() { + return config->mediaFolder % "/Named_Titles"; +} +*/ diff --git a/src/retroarch.h b/src/retroarch.h new file mode 100644 index 00000000..b87ea136 --- /dev/null +++ b/src/retroarch.h @@ -0,0 +1,67 @@ +/* + * This file is part of skyscraper. + * Copyright 2026 SineSwiper @ GitHub + * + * skyscraper is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * skyscraper is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with skyscraper; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +#ifndef RETROARCH_H +#define RETROARCH_H + +#include "abstractfrontend.h" + +#include + +class RetroArch : public AbstractFrontend { + Q_OBJECT + +public: + RetroArch(); + void assembleList(QString &finalOutput, + QList &gameEntries) override; + void skipExisting(QList &gameEntries, + QSharedPointer queue) override; + bool canSkip() override; + bool loadOldGameList(const QString &gameListFileString) override; + QString getGameListFileName() override; + QString getInputFolder() override; + QString getGameListFolder() override; + QString getMediaFolder() override; + QString getCoversFolder() override; + QString getScreenshotsFolder() override; + QString getMarqueesFolder() override; + QString getWheelsFolder() override; + + GameEntry::Types supportedMedia() override { + return GameEntry::Types(GameEntry::COVER | GameEntry::SCREENSHOT | + GameEntry::MARQUEE | GameEntry::WHEEL); + } + const QString getPlatformOutputName(); + +protected: + // Override to use game title (sanitized) instead of ROM baseName for + // media filenames + QString getTargetFileName(GameEntry::Types t, + const QString &baseName) override; + bool gamelistHasMediaPaths() override { return false; } + +private: + QString sanitizeForFilename(const QString &name); + QJsonObject createMetaProps(); + QMap baseNameToTitle; + QJsonObject existingPlaylist; +}; + +#endif // RETROARCH_H diff --git a/src/settings.cpp b/src/settings.cpp index 97ea4f47..28356fc6 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -247,7 +247,10 @@ void RuntimeCfg::applyConfigIni(CfgType type, QSettings *settings, type == CfgType::FRONTEND /* #68 */) ? PathTools::concatPath(v, config->platform) : v; - config->gameListFolder = toAbsolutePath(false, v); + // do not elaborate abs path for retroarch + config->gameListFolder = config->frontend == "retroarch" + ? config->gameListFolder + : toAbsolutePath(false, v); gameListFolderSet = true; continue; } @@ -353,6 +356,15 @@ void RuntimeCfg::applyConfigIni(CfgType type, QSettings *settings, config->platform = v; continue; } + if (k == "raExtra") { + if (config->frontend == "retroarch") { + config->frontendExtra = v.trimmed(); + } else { + ncprintf("\033[1;33mParameter raExtra is ignored. Only " + "applicable with frontend=retroarch.\n\033[0m"); + } + continue; + } if (k == "region") { config->region = v; continue; @@ -654,7 +666,7 @@ void RuntimeCfg::applyCli(bool &inputFolderSet, bool &gameListFolderSet, } } if (parser->isSet("e")) { - QStringList allowedFe({"attractmode", "pegasus"}); + QStringList allowedFe({"attractmode", "pegasus", "retroarch"}); if (allowedFe.contains(config->frontend)) { config->frontendExtra = parser->value("e"); } else { @@ -669,7 +681,10 @@ void RuntimeCfg::applyCli(bool &inputFolderSet, bool &gameListFolderSet, config->inputFolderNotMain = true; } if (parser->isSet("g")) { - config->gameListFolder = toAbsolutePath(true, parser->value("g")); + // do not elaborate abs path for retroarch + config->gameListFolder = config->frontend == "retroarch" + ? config->gameListFolder + : toAbsolutePath(true, parser->value("g")); gameListFolderSet = true; } if (parser->isSet("o")) { @@ -829,6 +844,14 @@ void RuntimeCfg::applyCli(bool &inputFolderSet, bool &gameListFolderSet, outOfRange("--verbosity", parser->value("verbosity").toInt()); } } + if (config->frontend == "retroarch" && !config->frontendExtra.isEmpty() && + (config->frontendExtra.split(";")).length() != 2) { + ncprintf( + "\033[1;33mValue of -e ... or raExtra=\"...\" has invalid " + "format '%s' and is ignored! Consult the documentation.\n\033[0m", + config->frontendExtra.toStdString().c_str()); + config->frontendExtra = ""; + } } void RuntimeCfg::setFlag(const QString flag) { @@ -961,7 +984,8 @@ QStringList RuntimeCfg::parseFlags() { bool RuntimeCfg::validateFrontend(const QString &providedFrontend) { QStringList frontends = {"emulationstation", "retrobat", "attractmode", - "pegasus", "esde", "batocera"}; + "pegasus", "esde", "batocera", + "retroarch"}; frontends.sort(); if (!frontends.contains(providedFrontend)) { ncprintf( diff --git a/src/settings.h b/src/settings.h index 0f24250d..f06cbce1 100644 --- a/src/settings.h +++ b/src/settings.h @@ -264,6 +264,7 @@ class RuntimeCfg : public QObject { {"onlyMissing", QPair("bool", CfgType::MAIN | CfgType::PLATFORM | CfgType::SCRAPER )}, {"platform", QPair("str", CfgType::MAIN )}, {"pretend", QPair("bool", CfgType::MAIN | CfgType::PLATFORM )}, + {"raExtra", QPair("str", CfgType::PLATFORM | CfgType::FRONTEND )}, {"region", QPair("str", CfgType::MAIN | CfgType::PLATFORM )}, {"regionPrios", QPair("str", CfgType::MAIN | CfgType::PLATFORM )}, {"relativePaths", QPair("bool", CfgType::MAIN | CfgType::PLATFORM )}, diff --git a/src/skyscraper.cpp b/src/skyscraper.cpp index 73354156..0234a0d9 100644 --- a/src/skyscraper.cpp +++ b/src/skyscraper.cpp @@ -36,6 +36,7 @@ #include "nocolor.h" #include "pathtools.h" #include "pegasus.h" +#include "retroarch.h" #include "settings.h" #include "strtools.h" @@ -128,13 +129,16 @@ void Skyscraper::run() { mediaSubFolderStdStr(config.screenshotsFolder).c_str()); ncprintf(" Wheels: '├── \033[1;32m%s\033[0m'\n", mediaSubFolderStdStr(config.wheelsFolder).c_str()); - ncprintf(" Marquees: '├── \033[1;32m%s\033[0m'\n", - mediaSubFolderStdStr(config.marqueesFolder).c_str()); bool notLast = config.videos || config.manuals || config.backcovers || config.fanart; - ncprintf(" Textures: '%s── \033[1;32m%s\033[0m'\n", - notLast ? "├" : "└", - mediaSubFolderStdStr(config.texturesFolder).c_str()); + ncprintf(" Marquees: '%s── \033[1;32m%s\033[0m'\n", + notLast || !config.texturesFolder.isEmpty() ? "├" : "└", + mediaSubFolderStdStr(config.marqueesFolder).c_str()); + if (!config.texturesFolder.isEmpty()) { + ncprintf(" Textures: '%s── \033[1;32m%s\033[0m'\n", + notLast ? "├" : "└", + mediaSubFolderStdStr(config.texturesFolder).c_str()); + } if (config.videos) { notLast = config.manuals || config.backcovers || config.fanart; ncprintf(" Videos: '%s── \033[1;32m%s\033[0m'\n", @@ -1007,6 +1011,8 @@ void Skyscraper::loadConfig(const QCommandLineParser &parser) { fePtr = new Esde(); } else if (config.frontend == "batocera") { fePtr = new Batocera(); + } else if (config.frontend == "retroarch") { + fePtr = new RetroArch(); } if (fePtr != nullptr) { frontend = QSharedPointer(fePtr); @@ -1021,40 +1027,59 @@ void Skyscraper::loadConfig(const QCommandLineParser &parser) { frontend->setConfig(&config); frontend->checkReqs(); - // Fallback to defaults if they aren't already set, find the rest in - // settings.h - if (config.frontend != "batocera") { - if (!inputFolderSet) + if (config.frontend == "retroarch") { + if (!inputFolderSet) { config.inputFolder = frontend->getInputFolder(); - if (!gameListFolderSet) - config.gameListFolder = frontend->getGameListFolder(); + } else { + validateAbsolutePath("inputFolder", config.inputFolder); + } + if (gameListFolderSet) { + validateAbsolutePath("gameListFolder", config.gameListFolder); + } + if (mediaFolderSet) { + validateAbsolutePath("mediaFolder", config.mediaFolder); + } + // do call these ignoring gameListFolderSet and mediaFolderSet + // as they will adjust the path to retroarch specs + config.gameListFolder = frontend->getGameListFolder(); + config.mediaFolder = frontend->getMediaFolder(); } else { - if (!gameListFolderSet) - config.gameListFolder = frontend->getGameListFolder(); - if (!inputFolderSet) - config.inputFolder = frontend->getInputFolder(); - } - if (!mediaFolderSet) { - if (config.frontend == "esde" || config.frontend == "batocera") { - config.mediaFolder = frontend->getMediaFolder(); + // Fallback to defaults if they aren't already set, find the rest in + // settings.h + if (config.frontend != "batocera") { + if (!inputFolderSet) + config.inputFolder = frontend->getInputFolder(); + if (!gameListFolderSet) + config.gameListFolder = frontend->getGameListFolder(); } else { - // defaults to /[.]media/ - QString mf = "media"; - if (config.mediaFolderHidden) { - mf = "." + mf; + // batocera (note the order) + if (!gameListFolderSet) + config.gameListFolder = frontend->getGameListFolder(); + if (!inputFolderSet) + config.inputFolder = frontend->getInputFolder(); + } + if (!mediaFolderSet) { + if (config.frontend == "esde" || config.frontend == "batocera") { + config.mediaFolder = frontend->getMediaFolder(); + } else { + // defaults to /[.]media/ + QString mf = "media"; + if (config.mediaFolderHidden) { + mf = "." + mf; + } + config.mediaFolder = + PathTools::concatPath(config.gameListFolder, mf); } - config.mediaFolder = - PathTools::concatPath(config.gameListFolder, mf); } } PathTools::expandHomePath(config.inputFolder); PathTools::expandHomePath(config.mediaFolder); - // defaults are always absolute, thus input- and mediafolder will be - // unchanged by these calls. - // gamelistfolder is absolute by now. - // the other two may be relative or absolute. if (config.frontend == "pegasus" || config.frontend == "batocera") { + // defaults are always absolute, thus input- and mediafolder will be + // unchanged by these calls. + // gamelistfolder is absolute by now. + // the other two may be relative or absolute. QString last = config.gameListFolder.split("/").last(); config.inputFolder = removeSurplusPlatformPath(config.platform, last, config.inputFolder); @@ -1064,19 +1089,11 @@ void Skyscraper::loadConfig(const QCommandLineParser &parser) { config.inputFolder); config.mediaFolder = PathTools::makeAbsolutePath(config.gameListFolder, config.mediaFolder); + } else if (config.frontend == "retroarch") { + ; // pass through, checks made above } else { - QFileInfo inputDirFileInfo = QFileInfo(config.inputFolder); - if (inputDirFileInfo.isRelative()) { - ncprintf("\033[1;31mBummer!\033[0m The parameter 'inputFolder' is " - "provided as relative path which is not valid for this " - "frontend. Provide the input folder as absolute path to " - "remediate. Now quitting...\n"); - emit die( - 1, "invalid frontend and input folder combination", - QString( - "Input folder may not be a relative path for frontend '%1'") - .arg(config.frontend)); - } + validateAbsolutePath("inputFolder", config.inputFolder); + const QFileInfo inputDirFileInfo = QFileInfo(config.inputFolder); QString last = config.inputFolder.split("/").last(); config.gameListFolder = removeSurplusPlatformPath( config.platform, last, config.gameListFolder); @@ -1268,6 +1285,24 @@ void Skyscraper::loadConfig(const QCommandLineParser &parser) { } } +void Skyscraper::validateAbsolutePath(const QString ¶m, + const QString &path) { + if (QFileInfo(path).isRelative()) { + ncprintf("\033[1;31mBummer!\033[0m The value of '%s' is " + "provided as relative path which is not valid for the " + "frontend '%s'. Provide '%s' as absolute path to " + "remediate. Now quitting...\n", + param.toStdString().c_str(), + config.frontend.toStdString().c_str(), + param.toStdString().c_str()); + emit die( + 1, "invalid frontend and path combination", + QString("path of '%1' may not be a relative path for frontend '%2'") + .arg(param) + .arg(config.frontend)); + } +} + QString Skyscraper::normalizePath(QFileInfo fileInfo) { // normalize paths for single romfiles provided at the CLI. // format will be: config.inputFolder + relative-path-of-romfile diff --git a/src/skyscraper.h b/src/skyscraper.h index b6eb53ff..53d567c4 100644 --- a/src/skyscraper.h +++ b/src/skyscraper.h @@ -106,6 +106,7 @@ private slots: const std::string mediaSubFolderStdStr(QString &in); QList readFileListFrom(const QString &filename); + void validateAbsolutePath(const QString ¶m, const QString &path); QSharedPointer frontend; QSharedPointer cache; diff --git a/supplementary/bash-completion/Skyscraper.bash b/supplementary/bash-completion/Skyscraper.bash index d7fffe5b..709b27be 100644 --- a/supplementary/bash-completion/Skyscraper.bash +++ b/supplementary/bash-completion/Skyscraper.bash @@ -78,7 +78,7 @@ _skyscraper() { ;; '-f') # frontends - mapfile -t COMPREPLY < <(compgen -W "emulationstation esde pegasus retrobat attractmode batocera" -- "$cur") + mapfile -t COMPREPLY < <(compgen -W "emulationstation esde pegasus retrobat attractmode batocera retroarch" -- "$cur") return 0 ;; '-t') diff --git a/supplementary/scraperdata/peas-schema.json b/supplementary/scraperdata/peas-schema.json index 70835203..995a5717 100644 --- a/supplementary/scraperdata/peas-schema.json +++ b/supplementary/scraperdata/peas-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Skyscraper's Platform, Extensions, Aliases and Scraper definition format (PEAS)", + "title": "Skyscraper's Platform, Extensions, Aliases and Scraper definition format (PEAS), extended with RetroArch information", "type": "object", "additionalProperties": { "type": "object", @@ -19,6 +19,10 @@ "type": "string", "pattern": "^\\*.[a-z0-9\\.]+$" } + }, + "retroarch_dbname": { + "type": "string", + "description": "Hint for RetroArch frontend which database to lookup for metadata." } }, "required": ["aliases", "formats"] diff --git a/test/retroarch/.gitignore b/test/retroarch/.gitignore new file mode 100644 index 00000000..4ea1efc0 --- /dev/null +++ b/test/retroarch/.gitignore @@ -0,0 +1,7 @@ +*~ +*.o +*.moc +Makefile +test_retroarch +platforms_idmap.csv +peas.json diff --git a/test/retroarch/playlists/test.lpl b/test/retroarch/playlists/test.lpl new file mode 100644 index 00000000..dc4b9901 --- /dev/null +++ b/test/retroarch/playlists/test.lpl @@ -0,0 +1,36 @@ +{ + "version": "1.5", + "default_core_path": "/opt/retropie/configs/all/retroarch/libretro/snes9x_libretro.so", + "default_core_name": "snes9x", + "label_display_mode": "0", + "left_display_mode": "0", + "right_display_mode": "0", + "thumbnail_match_mode": "0", + "sort_mode": "0", + "items": [ + { + "path": "/home/pi/RetroPie/roms/snes/Game One.sfc", + "label": "Game One Title", + "core_path": "/opt/retropie/configs/all/retroarch/libretro/snes9x_libretro.so", + "core_name": "snes9x", + "crc32": "DETECT", + "db_name": "Nintendo - Super Nintendo Entertainment System.lpl" + }, + { + "path": "/home/pi/RetroPie/roms/snes/Game Two.zip", + "label": "Super Game Two", + "core_path": "/opt/retropie/configs/all/retroarch/libretro/bsnes_libretro.so", + "core_name": "bsnes", + "crc32": "DETECT", + "db_name": "Nintendo - Super Nintendo Entertainment System.lpl" + }, + { + "path": "/home/pi/RetroPie/roms/snes/Mega Man.sfc", + "label": "Mega Man", + "core_path": "DETECT", + "core_name": "DETECT", + "crc32": "DETECT", + "db_name": "Nintendo - Super Nintendo Entertainment System.lpl" + } + ] +} \ No newline at end of file diff --git a/test/retroarch/test_retroarch.cpp b/test/retroarch/test_retroarch.cpp new file mode 100644 index 00000000..ef6a001a --- /dev/null +++ b/test/retroarch/test_retroarch.cpp @@ -0,0 +1,405 @@ +#include "platform.h" +#include "retroarch.h" + +#include +#include +#include +#include +#include +#include +#include + +class TestRetroArch : public QObject { + Q_OBJECT + +private: + Settings settings; + RetroArch *frontend; + +private slots: + void initTestCase() { + frontend = new RetroArch(); + frontend->setConfig(&settings); + if (!Platform::get().loadConfig()) { + qWarning() << "*** AIEEE !!!\n"; + exit(1); + } + } + + void cleanupTestCase() { delete frontend; } + + // Test platform output name resolution through public interface + void testGetPlatformOutputName() { + // Test fallback behavior for unknown platform + settings.platform = "UnknownPlatform12345"; + QString result = frontend->getPlatformOutputName(); + QCOMPARE(result, "UnknownPlatform12345"); // Falls back to platform name + + // Test with known platform (depends on peas.json) + settings.platform = "snes"; + result = frontend->getPlatformOutputName(); + QCOMPARE(result, "Nintendo - Super Nintendo Entertainment System"); + } + + // Test loading existing playlist files for incremental updates + void testLoadOldGameList() { + // Test valid playlist + bool result = frontend->loadOldGameList("playlists/test.lpl"); + QVERIFY(result); + + // Verify entries were loaded by checking the oldEntries list size + // We'll access this indirectly through other tests or add a getter if + // needed + + // Test non-existent file + QCOMPARE(frontend->loadOldGameList("/nonexistent/file.lpl"), false); + + // Test invalid JSON (create temp file with bad content) + QFile badFile("playlists/invalid.txt"); + QVERIFY(badFile.open(QIODevice::WriteOnly)); + QTextStream out(&badFile); + out << "not json at all"; + badFile.close(); + QCOMPARE(frontend->loadOldGameList("playlists/invalid.txt"), false); + + // Test valid JSON but wrong structure (no "items" array) + QFile weirdFile("playlists/weird.json"); + QVERIFY(weirdFile.open(QIODevice::WriteOnly)); + QTextStream out2(&weirdFile); + out2 << "{\"version\": \"1.5\", \"data\": []}"; + weirdFile.close(); + // This should return true (it's valid JSON with an object root) + // but won't load any entries since there's no "items" array + result = frontend->loadOldGameList("playlists/weird.json"); + QVERIFY(result); + + // Cleanup temp files + QFile::remove("playlists/invalid.txt"); + QFile::remove("playlists/weird.json"); + } + + // Test the main playlist generation functionality through public interface + void testAssembleList() { + RetroArch localFrontend; // Fresh instance without old entries + Settings localSettings; + localFrontend.setConfig(&localSettings); + + // Create test game entries + QList entries; + + GameEntry entry1; + entry1.path = "/roms/snes/Mega Man.sfc"; + entry1.title = "Mega Man"; + entry1.baseName = "Mega Man"; + entry1.absoluteFilePath = "/roms/snes/Mega Man.sfc"; + entries.append(entry1); + + GameEntry entry2; + entry2.path = "/roms/snes/Zelda.zip"; + entry2.title = "The Legend of Zelda"; + entry2.baseName = "Zelda"; + entry2.absoluteFilePath = "/roms/snes/Zelda.zip"; + entries.append(entry2); + + // Set platform for db_name resolution + localSettings.platform = "snes"; + + // Assemble the list + QString output; + localFrontend.assembleList(output, entries); + + // Parse and verify JSON structure + QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); + QVERIFY(doc.isObject()); + QJsonObject root = doc.object(); + + // Check version (should be 1.5) + QCOMPARE(root.value("version").toString(), "1.5"); + + // Check default_core_path and default_core_name defaults + QCOMPARE(root.value("default_core_path").toString(), "DETECT"); + QCOMPARE(root.value("default_core_name").toString(), "DETECT"); + + // Check display modes + QCOMPARE(root.value("label_display_mode").toInt(), 0); + QCOMPARE(root.value("left_display_mode").toInt(), 0); + QCOMPARE(root.value("right_display_mode").toInt(), 0); + QCOMPARE(root.value("thumbnail_match_mode").toInt(), 0); + QCOMPARE(root.value("sort_mode").toInt(), 0); + + // Check items array + QJsonArray items = root.value("items").toArray(); + QCOMPARE(items.size(), 2); + + // Verify first item structure + QJsonObject firstItem = items.at(0).toObject(); + QCOMPARE(firstItem.value("path").toString(), "/roms/snes/Mega Man.sfc"); + QCOMPARE(firstItem.value("label").toString(), "Mega Man"); + QCOMPARE(firstItem.value("core_path").toString(), "DETECT"); + QCOMPARE(firstItem.value("core_name").toString(), "DETECT"); + QCOMPARE(firstItem.value("crc32").toString(), "DETECT"); + QVERIFY(firstItem.value("db_name").toString().endsWith(".lpl")); + + // Verify second item structure + QJsonObject secondItem = items.at(1).toObject(); + QCOMPARE(secondItem.value("path").toString(), "/roms/snes/Zelda.zip"); + QCOMPARE(secondItem.value("label").toString(), "The Legend of Zelda"); + } + + // Test that special characters in game titles are handled properly + // (indirectly tests sanitizeForFilename via getTargetFileName) + void testSpecialCharactersInTitles() { + RetroArch localFrontend; + Settings localSettings; + localFrontend.setConfig(&localSettings); + + QList entries; + + // Entry with special characters in title that need sanitization for + // filenames + GameEntry entry; + entry.path = "/roms/snes/SpecialGame.sfc"; + entry.title = "Special & Game! With Forbidden/Chars"; + entry.baseName = "SpecialGame"; + entry.absoluteFilePath = "/roms/snes/SpecialGame.sfc"; + entries.append(entry); + + QString output; + localFrontend.assembleList(output, entries); + + // Verify the playlist was created correctly with special chars in title + QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); + QJsonObject root = doc.object(); + QJsonArray items = root.value("items").toArray(); + QCOMPARE(items.size(), 1); + QJsonObject item = items.at(0).toObject(); + + // Title should be preserved in the label (JSON handles special chars + // natively) + QCOMPARE(item.value("label").toString(), + "Special & Game! With Forbidden/Chars"); + + // Verify JSON is valid and parseable (indirectly tests that output is + // well-formed) + QVERIFY(doc.isObject()); + } + + // Test the playlist filename generation through public interface + void testGetGameListFileName() { + Settings localSettings; + RetroArch localFrontend; + localFrontend.setConfig(&localSettings); + + // Default case - uses platform output name + .lpl extension + localSettings.platform = "snes"; + localSettings.gameListFilename.clear(); + QString result = localFrontend.getGameListFileName(); + QVERIFY(result.endsWith(".lpl")); + + // Custom filename from config + localSettings.gameListFilename = "custom_playlist.lpl"; + QCOMPARE(localFrontend.getGameListFileName(), "custom_playlist.lpl"); + + // Another custom format test + localSettings.gameListFilename = "my_games.json"; + QCOMPARE(localFrontend.getGameListFileName(), "my_games.json"); + } + + // Test all the folder path getter functions through public interface + void testFolderPaths() { + Settings localSettings; + RetroArch localFrontend; + localFrontend.setConfig(&localSettings); + + localSettings.platform = "snes"; + QString ra_db_name = localFrontend.getPlatformOutputName(); + + QString expMediaFolder = + QDir::homePath() % "/.config/retroarch/thumbnails/" % ra_db_name; + // Expect default if no mediaFolder= was set + localSettings.mediaFolder = localFrontend.getMediaFolder(); + QCOMPARE(localFrontend.getMediaFolder(), expMediaFolder); + + // Test covers folder (Named_Boxarts subfolder of config->mediaFolder) + QCOMPARE(localFrontend.getCoversFolder(), + expMediaFolder % "/Named_Boxarts"); + + // Test screenshots folder (Named_Snaps subfolder) + QCOMPARE(localFrontend.getScreenshotsFolder(), + expMediaFolder % "/Named_Snaps"); + + // Test marquees/wheels folder (Named_Logos subfolder) + QCOMPARE(localFrontend.getMarqueesFolder(), + expMediaFolder % "/Named_Logos"); + QCOMPARE(localFrontend.getWheelsFolder(), + expMediaFolder % "/Named_Logos"); + + // Test with different media folder + localSettings.mediaFolder = "/yadda/yadda/downloaded_media/snes"; + // replace "/snes" + localSettings.mediaFolder = localFrontend.getMediaFolder(); + QCOMPARE(localFrontend.getCoversFolder(), + "/yadda/yadda/downloaded_media/" % ra_db_name % + "/Named_Boxarts"); + } + + // Test that the correct media types are supported through public interface + void testSupportedMedia() { + GameEntry::Types supported = frontend->supportedMedia(); + + // Should support COVER, SCREENSHOT, and WHEEL + QVERIFY(supported & GameEntry::COVER); + QVERIFY(supported & GameEntry::SCREENSHOT); + QVERIFY(supported & GameEntry::WHEEL); + + // Should NOT support VIDEO (not yet implemented) + QVERIFY(!(supported & GameEntry::VIDEO)); + + // Should NOT support MANUAL, TEXTURE, FANART, BACKCOVER + QVERIFY(!(supported & GameEntry::MANUAL)); + QVERIFY(!(supported & GameEntry::TEXTURE)); + QVERIFY(!(supported & GameEntry::FANART)); + QVERIFY(!(supported & GameEntry::BACKCOVER)); + } + + // Test frontend capabilities flags through public interface + void testFrontendCapabilities() { + // Can skip existing entries for incremental updates + QVERIFY(frontend->canSkip()); + } + + // Test empty game list handling through public interface + void testEmptyGameList() { + RetroArch localFrontend; + Settings localSettings; + localFrontend.setConfig(&localSettings); + + QList entries; // Empty list + localSettings.platform = "snes"; + + QString output; + localFrontend.assembleList(output, entries); + + // assembleList has early return for empty lists - no output is produced + // This prevents creating empty playlist files when there are no games + QVERIFY(output.isEmpty()); + } + + // Test frontendExtra parsing for core path info (contains /) through public + // interface + void testFrontendExtraCorePath() { + RetroArch localFrontend; + Settings localSettings; + localFrontend.setConfig(&localSettings); + + QList entries; + GameEntry entry; + entry.path = "/roms/test.rom"; + entry.title = "Test Game"; + entry.baseName = "test"; + entry.absoluteFilePath = "/roms/test.rom"; + entries.append(entry); + + // Test frontendExtra with core path info (contains /) + localSettings.frontendExtra = "/path/to/core;mycore"; + localSettings.platform = "testplatform"; + + QString output; + localFrontend.assembleList(output, entries); + + QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); + QJsonObject root = doc.object(); + + // When frontendExtra contains /, it's parsed as core path info + // The first part becomes default_core_path, second (after ;) becomes + // default_core_name + QCOMPARE(root.value("default_core_path").toString(), "/path/to/core"); + QCOMPARE(root.value("default_core_name").toString(), "mycore"); + + // db_name is determined by platform mapping, not frontendExtra + QJsonArray items = root.value("items").toArray(); + QVERIFY(items.size() > 0); + QCOMPARE(items.at(0).toObject().value("db_name").toString(), + "testplatform.lpl"); + } + + // Test that JSON output is valid with various game title formats + void testGameTitleFormats() { + RetroArch localFrontend; + Settings localSettings; + localFrontend.setConfig(&localSettings); + localSettings.platform = "snes"; + + QList entries; + + // Game with quotes in title + GameEntry entry1; + entry1.path = "/roms/snes/GameOne.sfc"; + entry1.title = "Game's \"Title\""; + entry1.baseName = "GameOne"; + entry1.absoluteFilePath = "/roms/snes/GameOne.sfc"; + entries.append(entry1); + + // Game with newlines (unusual but possible) + GameEntry entry2; + entry2.path = "/roms/snes/GameTwo.sfc"; + entry2.title = "Game Two\nSubtitle"; + entry2.baseName = "GameTwo"; + entry2.absoluteFilePath = "/roms/snes/GameTwo.sfc"; + entries.append(entry2); + + // Game with backslash in path + GameEntry entry3; + entry3.path = "/roms/snes/Path\\Test.sfc"; + entry3.title = "Backslash Path Test"; + entry3.baseName = "Path\\Test"; + entry3.absoluteFilePath = "/roms/snes/Path\\Test.sfc"; + entries.append(entry3); + + QString output; + localFrontend.assembleList(output, entries); + + // Verify all JSON is valid and can be parsed back + QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8()); + QVERIFY(doc.isObject()); + + QJsonObject root = doc.object(); + QJsonArray items = root.value("items").toArray(); + QCOMPARE(items.size(), 3); + + // Verify titles are preserved correctly (JSON escaping handles them) + QCOMPARE(items.at(0).toObject().value("label").toString(), + "Game's \"Title\""); + QCOMPARE(items.at(1).toObject().value("label").toString(), + "Game Two\nSubtitle"); + QCOMPARE(items.at(2).toObject().value("label").toString(), + "Backslash Path Test"); + } + + // Test input folder path through public interface + void testInputFolder() { + Settings localSettings; + RetroArch localFrontend; + localFrontend.setConfig(&localSettings); + + localSettings.platform = "snes"; + + // Input folder should be ~/RetroPie/roms/ + QString inputPath = localFrontend.getInputFolder(); + QVERIFY(inputPath.contains("/RetroPie/roms/snes")); + } + + // Test game list folder path through public interface + void testGameListFolder() { + Settings localSettings; + RetroArch localFrontend; + localFrontend.setConfig(&localSettings); + + // Game list folder should be the standard RetroArch playlist location + QCOMPARE(localFrontend.getGameListFolder(), + QDir::homePath() % "/.config/retroarch/playlists"); + } +}; + +QTEST_MAIN(TestRetroArch) +#include "test_retroarch.moc" diff --git a/test/retroarch/test_retroarch.pro b/test/retroarch/test_retroarch.pro new file mode 100644 index 00000000..82d351f8 --- /dev/null +++ b/test/retroarch/test_retroarch.pro @@ -0,0 +1,43 @@ +QT += core network xml testlib +TEMPLATE = app +TARGET = test_retroarch +DEPENDPATH += . +INCLUDEPATH += ../../src +CONFIG += debug +QMAKE_CXXFLAGS += -std=c++17 + +CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT +PREFIX = /usr/local +SYSCONFDIR = $${PREFIX}/etc +DEFINES+=PREFIX=\\\"$$PREFIX\\\" +DEFINES+=SYSCONFDIR=\\\"$$SYSCONFDIR\\\" + +QMAKE_POST_LINK += cp -f $$shell_quote($$shell_path($${PWD}/../../peas.json)) .; +QMAKE_POST_LINK += cp -f $$shell_quote($$shell_path($${PWD}/../../platforms_idmap.csv)) .; + +include(../../VERSION.ini) +DEFINES+=TESTING +DEFINES+=VERSION=\\\"$$VERSION\\\" + +HEADERS += \ + ../../src/retroarch.h \ + ../../src/abstractfrontend.h \ + ../../src/config.h \ + ../../src/gameentry.h \ + ../../src/nocolor.h \ + ../../src/pathtools.h \ + ../../src/platform.h \ + ../../src/strtools.h + +SOURCES += test_retroarch.cpp \ + ../../src/retroarch.cpp \ + ../../src/abstractfrontend.cpp \ + ../../src/config.cpp \ + ../../src/gameentry.cpp \ + ../../src/nocolor.cpp \ + ../../src/pathtools.cpp \ + ../../src/platform.cpp \ + ../../src/strtools.cpp \ + +# Test data directory +DISTFILES += playlists/test.lpl \ No newline at end of file