diff --git a/README.md b/README.md index 5c3b8d8..e5b3f8e 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ usually `/data/homebrew/PPSA99169/`. | Folder or file | Purpose | | --- | --- | | `content/` | Your content; available to WebUI uploads and downloads | -| `content/Saturn/` | Saturn disc images; created automatically | +| `content//` | A folder for each installed system, named as EmulationStation (ES-DE) names it: `psx`, `ps2`, `snes`, `saturn`; created automatically | | `system/` | General BIOS and system data | | `system/Saturn/` | Saturn BIOS files; created automatically | | `system/pcsx2/bios/` | PlayStation 2 BIOS (your own dump) | diff --git a/src/main.cpp b/src/main.cpp index f5d31d4..c5763e7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -406,7 +406,13 @@ int main(int process_argc, char **process_argv) * screen the picker, which restarts the title as RetroArch or EmulationStation; * those two are their own executables, started through LoadExec. */ /* A folder in content for each system the installed cores run, before any frontend: - * a launch from the home screen goes to the picker from here (issue 33). */ + * a launch from the home screen goes to the picker from here (issue 33). Folders + * earlier builds named as RetroArch does are renamed as ES-DE names them first. */ + const int renamed_folders = + ps5_library_rename_system_folders("/app0/content", "/app0/playlists", "/app0/config"); + if (renamed_folders > 0) + ps5::debug::mark_value("startup: system folders renamed as ES-DE names them", + renamed_folders); const int system_folders = ps5_library_make_system_folders("/app0/content", "/app0/info", "/app0/cores"); if (system_folders > 0) diff --git a/src/ps5_library.c b/src/ps5_library.c index 247bed8..3c628ee 100644 --- a/src/ps5_library.c +++ b/src/ps5_library.c @@ -19,6 +19,7 @@ #include #include #include +#include /* --- the JSON RetroArch writes its playlists in ----------------------------------- */ @@ -1470,6 +1471,9 @@ int ps5_library_load_content_at(struct ps5_library *library, const char *playlis int ps5_library_make_system_folders(const char *content, const char *info, const char *cores) { + /* An install copied without its empty folders has no content folder at all */ + if (mkdir(content, 0777) == 0) + chmod(content, 0777); /* FTP's reach: the title's folders are 0777 */ /* The systems content already has a folder for (top level), by platform. */ char **names = folder_names(content, ""); if (!names) @@ -1505,8 +1509,7 @@ int ps5_library_make_system_folders(const char *content, const char *info, const if (!platform || !platform->database || has[platform - platforms]) continue; char path[PS5_LIBRARY_PATH_MAX]; - if (snprintf(path, sizeof(path), "%s/%s", content, platform->database) >= - (int)sizeof(path)) + if (snprintf(path, sizeof(path), "%s/%s", content, platform->id) >= (int)sizeof(path)) continue; has[platform - platforms] = 1; const int created = mkdir(path, 0777) == 0; @@ -1518,6 +1521,200 @@ int ps5_library_make_system_folders(const char *content, const char *info, const return made; } +static int is_folder(const char *path) +{ + struct stat status; + return stat(path, &status) == 0 && S_ISDIR(status.st_mode); +} + +static int is_there(const char *path) +{ + struct stat status; + return lstat(path, &status) == 0; +} + +/* Moves every entry of from into to, none of whose names may be taken there, and removes + * from. 0; -1 with everything back where it was. */ +static int move_entries(const char *from, const char *to) +{ + char **names = folder_names(from, ""); + if (!names) + return -1; + char source[PS5_LIBRARY_PATH_MAX], target[PS5_LIBRARY_PATH_MAX]; + size_t moved = 0, count = 0; + int failed = 0; + for (; names[count]; count++) + if (strcmp(names[count], ".") != 0 && strcmp(names[count], "..") != 0 && + (!joined(source, sizeof(source), from, names[count]) || + !joined(target, sizeof(target), to, names[count]) || is_there(target))) + failed = 1; + for (; !failed && moved < count; moved++) + if (strcmp(names[moved], ".") != 0 && strcmp(names[moved], "..") != 0 && + (!joined(source, sizeof(source), from, names[moved]) || + !joined(target, sizeof(target), to, names[moved]) || rename(source, target) != 0)) + failed = 1; + if (failed) + while (moved-- > 0) /* the ones moved before the failure go back */ + if (strcmp(names[moved], ".") != 0 && strcmp(names[moved], "..") != 0 && + joined(source, sizeof(source), from, names[moved]) && + joined(target, sizeof(target), to, names[moved])) + rename(target, source); + free_names(names); + if (failed) + return -1; + rmdir(from); /* everything moved: the paths change whether or not it goes */ + return 0; +} + +/* Each "/" in a file that is followed by '/' or '"' (a game's path, or a + * playlist's scan folder) becomes "/"; the file is replaced only when that + * changed something. */ +static void rewrite_paths(const char *file, const char *content, const char *const *old_names, + const char *const *new_names, size_t pairs) +{ + FILE *input = fopen(file, "rb"); + if (!input) + return; + char *text = NULL; + long length = 0; + if (fseek(input, 0, SEEK_END) == 0 && (length = ftell(input)) > 0 && length < (64L << 20) && + fseek(input, 0, SEEK_SET) == 0 && (text = (char *)malloc((size_t)length)) != NULL && + fread(text, 1, (size_t)length, input) != (size_t)length) + { + free(text); + text = NULL; + } + fclose(input); + if (!text) + return; + const size_t content_length = strlen(content); + size_t longest = 0; + for (size_t p = 0; p < pairs; p++) + if (strlen(new_names[p]) > longest) + longest = strlen(new_names[p]); + /* each match is at least content_length + 2 long and grows by at most longest */ + char *out = + (char *)malloc((size_t)length + ((size_t)length / (content_length + 2) + 1) * longest); + size_t written = 0, changes = 0; + for (long at = 0; out && at < length;) + { + size_t p = pairs; + if ((size_t)(length - at) > content_length && + memcmp(text + at, content, content_length) == 0 && + text[at + (long)content_length] == '/') + for (p = 0; p < pairs; p++) + { + const size_t old_length = strlen(old_names[p]); + const long end = at + (long)(content_length + 1 + old_length); + if (end < length && (text[end] == '/' || text[end] == '"') && + memcmp(text + at + content_length + 1, old_names[p], old_length) == 0) + break; + } + if (p == pairs) + { + out[written++] = text[at++]; + continue; + } + const size_t new_length = strlen(new_names[p]); + memcpy(out + written, content, content_length); + out[written + content_length] = '/'; + memcpy(out + written + content_length + 1, new_names[p], new_length); + written += content_length + 1 + new_length; + at += (long)(content_length + 1 + strlen(old_names[p])); + changes++; + } + free(text); + char temporary[PS5_LIBRARY_PATH_MAX]; + FILE *output = NULL; + if (out && changes && + snprintf(temporary, sizeof(temporary), "%s.tmp", file) < (int)sizeof(temporary) && + (output = fopen(temporary, "wb")) != NULL) + { + const int complete = fwrite(out, 1, written, output) == written; + if (fclose(output) == 0 && complete && rename(temporary, file) == 0) + chmod(file, 0777); /* FTP's reach: the title's files are 0777 */ + else + remove(temporary); + } + free(out); +} + +static void rewrite_playlists(const char *folder, const char *content, const char *const *old_names, + const char *const *new_names, size_t pairs) +{ + char **names = folder_names(folder, ".lpl"); + char path[PS5_LIBRARY_PATH_MAX]; + for (size_t i = 0; names && names[i]; i++) + if (joined(path, sizeof(path), folder, names[i])) + rewrite_paths(path, content, old_names, new_names, pairs); + free_names(names); +} + +/* RetroArch's settings for a content folder, named after it, in each core's folder under + * root: content directory overrides and core options, shader presets, remaps. */ +static void rename_folder_settings(const char *root, const char *const *old_names, + const char *const *new_names, size_t pairs) +{ + static const char *const kinds[] = {".cfg", ".opt", ".slangp", ".glslp", ".cgp", ".rmp"}; + char **cores = folder_names(root, ""); + char folder[PS5_LIBRARY_PATH_MAX], from[PS5_LIBRARY_PATH_MAX], to[PS5_LIBRARY_PATH_MAX]; + for (size_t c = 0; cores && cores[c]; c++) + { + if (cores[c][0] == '.' || !joined(folder, sizeof(folder), root, cores[c]) || + !is_folder(folder)) + continue; + for (size_t p = 0; p < pairs; p++) + for (size_t k = 0; + k < sizeof(kinds) / sizeof(kinds[0]) && strcmp(cores[c], old_names[p]) != 0; k++) + if (snprintf(from, sizeof(from), "%s/%s%s", folder, old_names[p], kinds[k]) < + (int)sizeof(from) && + snprintf(to, sizeof(to), "%s/%s%s", folder, new_names[p], kinds[k]) < + (int)sizeof(to) && + is_file(from) && !is_there(to)) + rename(from, to); + } + free_names(cores); +} + +int ps5_library_rename_system_folders(const char *content, const char *playlists, + const char *config) +{ + char **names = folder_names(content, ""); + if (!names) + return -1; + const char *old_names[sizeof(platforms) / sizeof(platforms[0])]; + const char *new_names[sizeof(platforms) / sizeof(platforms[0])]; + size_t pairs = 0; + char from[PS5_LIBRARY_PATH_MAX], to[PS5_LIBRARY_PATH_MAX]; + for (size_t i = 0; names[i]; i++) + { + const struct platform *platform = NULL; + for (size_t p = 0; !platform && p < sizeof(platforms) / sizeof(platforms[0]); p++) + if (platforms[p].database && strcmp(platforms[p].database, names[i]) == 0) + platform = &platforms[p]; + if (!platform || !joined(from, sizeof(from), content, names[i]) || + !joined(to, sizeof(to), content, platform->id) || !is_folder(from)) + continue; + if (is_there(to) ? !is_folder(to) || move_entries(from, to) != 0 : rename(from, to) != 0) + continue; + chmod(to, 0777); /* FTP's reach: the title's folders are 0777 */ + old_names[pairs] = platform->database; + new_names[pairs++] = platform->id; + } + free_names(names); + if (pairs) + { + char folder[PS5_LIBRARY_PATH_MAX]; + rewrite_playlists(playlists, content, old_names, new_names, pairs); + if (joined(folder, sizeof(folder), playlists, "builtin")) + rewrite_playlists(folder, content, old_names, new_names, pairs); + rename_folder_settings(config, old_names, new_names, pairs); + if (joined(folder, sizeof(folder), config, "remaps")) + rename_folder_settings(folder, old_names, new_names, pairs); + } + return (int)pairs; +} + void ps5_library_free(struct ps5_library *library) { free(library->systems); diff --git a/src/ps5_library.h b/src/ps5_library.h index 8a21ca5..63e9fda 100644 --- a/src/ps5_library.h +++ b/src/ps5_library.h @@ -125,13 +125,24 @@ extern "C" /* A platform's RetroArch database name ("Sony - PlayStation"), or NULL. */ const char *ps5_library_platform_database(const char *id); - /* A folder in content for each system the installed cores run, named as RetroArch - * names it ("Nintendo - Nintendo Entertainment System"), so games have a place to go - * and every frontend knows their system. A system the content folder already has a - * folder for, whatever its name ("PS1", "Genesis"), gets none: nothing is renamed or - * moved. A database no platform here stands for (Satellaview, CD32...) gets none. + /* A folder in content for each system the installed cores run, named as ES-DE names + * it ("nes", "ps2", "megadrive"... its system's id here), so games have a place to go + * and every frontend knows their system. content is made first when it is missing. A + * system the content folder already has a folder for, whatever its name ("PS1"), gets + * none. A database no platform here stands for (Satellaview, CD32...) gets none. * Returns how many folders it made, or -1 when content cannot be read. */ int ps5_library_make_system_folders(const char *content, const char *info, const char *cores); + /* The folders earlier builds made under RetroArch's names ("Sony - PlayStation 2") + * renamed to ES-DE's ("ps2"), with what refers to them: the paths in the playlists + * (playlists and playlists/builtin) and the per-folder overrides, core options, shader + * presets and remaps under config (config/, config/remaps/). Where the + * ES-DE folder exists, the old folder's entries move into it when none of their names + * is taken there, and the old folder goes; otherwise it is left as it is (it is still + * its system's). A core folder named as the content folder keeps its files, which are + * that core's own. Returns how many folders it renamed or merged, or -1 when content + * cannot be read. */ + int ps5_library_rename_system_folders(const char *content, const char *playlists, + const char *config); /* Game mode's launch command for a game (src/ps5_game.h): RetroArch's command line * with its system's core, shell-quoted. 0, or -1 when it does not fit. */ diff --git a/src/scraper.cpp b/src/scraper.cpp index d0ffbf7..28b3235 100644 --- a/src/scraper.cpp +++ b/src/scraper.cpp @@ -2977,7 +2977,7 @@ bool add_target(const std::string &system, const std::string &filename, std::str why = "This file type is not supported by the installed cores for this system."; return false; } - relative = selected->second.at("name") + '/' + filename; + relative = selected->first + '/' + filename; /* the system's folder is its ES-DE name */ key = game_key("/app0/content/" + relative); for (const auto &game : load_games()) if (game.system == system && game.key == key) diff --git a/tests/ps5_library_test.c b/tests/ps5_library_test.c index 6ef7c07..9ed76a1 100644 --- a/tests/ps5_library_test.c +++ b/tests/ps5_library_test.c @@ -42,6 +42,16 @@ static void core(const char *dir, const char *name, const char *database, const write_text(path, text); } +/* A file's text, at most size - 1 bytes of it. */ +static const char *read_back(const char *path, char *out, size_t size) +{ + FILE *file = fopen(path, "rb"); + assert(file); + out[fread(out, 1, size - 1, file)] = '\0'; + fclose(file); + return out; +} + static const struct ps5_library_system *system_of(const struct ps5_library *library, const char *id) { for (size_t i = 0; i < library->system_count; i++) @@ -368,8 +378,8 @@ int main(int argc, char **argv) assert(system_of(&library, "snes")->game_count == 2); ps5_library_free(&library); - /* A folder for each system the installed cores run, named as RetroArch names it; a - * system with a folder already (any name) gets none, and nothing is renamed. */ + /* A folder for each system the installed cores run, named as ES-DE names it; a system + * with a folder already (any name) gets none. */ { char root[1024], content[1100], info[1100], cores[1100], path[2048]; snprintf(root, sizeof(root), "%s/folders", dir); @@ -400,12 +410,8 @@ int main(int argc, char **argv) snprintf(path, sizeof(path), "%s/stella_libretro.info", info); write_text(path, "database = \"Atari - 2600\"\n"); /* no core beside it: not installed */ assert(ps5_library_make_system_folders(content, info, cores) == 10); - const char *expected[] = { - "Sega - Game Gear", "Sega - Master System - Mark III", - "Sega - Mega-CD - Sega CD", "Sega - Mega Drive - Genesis", - "Sega - SG-1000", "Nintendo - Super Nintendo Entertainment System", - "Nintendo - GameCube", "Nintendo - Wii", - "Commodore - Amiga", "FBNeo - Arcade Games"}; + const char *expected[] = {"gamegear", "mastersystem", "segacd", "genesis", "sg-1000", + "snes", "gc", "wii", "amiga", "fbneo"}; for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); i++) { struct stat st; @@ -414,10 +420,16 @@ int main(int argc, char **argv) /* Each one is its system's to every frontend. */ assert(ps5_library_platform(expected[i])); } - const char *absent[] = {"Sony - PlayStation", "Nintendo - Sufami Turbo", - "Nintendo - Satellaview", "Sega - PICO", - "Commodore - CD32", "Atari - 2600", - "Nintendo - Wii (Digital)"}; + const char *absent[] = {"Sony - PlayStation", + "psx", + "Nintendo - Sufami Turbo", + "Nintendo - Satellaview", + "Sega - PICO", + "Commodore - CD32", + "Atari - 2600", + "atari2600", + "Nintendo - Wii (Digital)", + "Sega - Mega Drive - Genesis"}; for (size_t i = 0; i < sizeof(absent) / sizeof(absent[0]); i++) { struct stat st; @@ -425,10 +437,130 @@ int main(int argc, char **argv) assert(stat(path, &st) != 0); } assert(ps5_library_make_system_folders(content, info, cores) == 0); /* once */ + /* An install copied without its empty folders: content is made too */ snprintf(path, sizeof(path), "%s/missing", root); + assert(ps5_library_make_system_folders(path, info, cores) == 11); + snprintf(path, sizeof(path), "%s/missing/psx", root); + assert(stat(path, &(struct stat){0}) == 0); + snprintf(path, sizeof(path), "%s/no/such/content", root); assert(ps5_library_make_system_folders(path, info, cores) == -1); } + /* Folders earlier builds named as RetroArch does, renamed as ES-DE names them, with the + * playlists' paths and the per-folder settings that name them. */ + { + char root[1024], content[1100], playlists[1100], config[1100], path[2048], text[4096], + want[2048]; + snprintf(root, sizeof(root), "%s/renamed", dir); + snprintf(content, sizeof(content), "%s/content", root); + snprintf(playlists, sizeof(playlists), "%s/playlists", root); + snprintf(config, sizeof(config), "%s/config", root); + const char *folders[] = {"", + "/content", + "/content/Sony - PlayStation 2", + "/content/Nintendo - Game Boy", + "/content/gb", + "/content/Nintendo - Game Boy Advance", + "/content/gba", + "/content/MAME", + "/content/PS1", + "/playlists", + "/playlists/builtin", + "/config", + "/config/PS5SX2", + "/config/MAME", + "/config/remaps", + "/config/remaps/PS5SX2"}; + for (size_t i = 0; i < sizeof(folders) / sizeof(folders[0]); i++) + { + snprintf(path, sizeof(path), "%s%s", root, folders[i]); + assert(mkdir(path, 0755) == 0); + } + const char *files[] = {"content/Sony - PlayStation 2/Disc.iso", + "content/Nintendo - Game Boy/One.gb", + "content/gb/Two.gb", + "content/Nintendo - Game Boy Advance/Same.gba", + "content/gba/Same.gba", + "content/MAME/game.zip", + "content/PS1/Disc.cue", + "config/PS5SX2/Sony - PlayStation 2.cfg", + "config/PS5SX2/Sony - PlayStation 2.opt", + "config/remaps/PS5SX2/Sony - PlayStation 2.rmp", + "config/MAME/MAME.cfg", + "config/MAME/MAME.slangp"}; + for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); i++) + { + snprintf(path, sizeof(path), "%s/%s", root, files[i]); + write_text(path, "x"); + } + snprintf(path, sizeof(path), "%s/Sony - PlayStation 2.lpl", playlists); + snprintf(text, sizeof(text), + "{\"scan_content_dir\": \"%s/Sony - PlayStation 2\", \"items\": [{\"path\": " + "\"%s/Sony - PlayStation 2/Disc.iso\"}]}", + content, content); + write_text(path, text); + snprintf(path, sizeof(path), "%s/Nintendo - Game Boy.lpl", playlists); + snprintf(text, sizeof(text), + "{\"items\": [{\"path\": \"%s/Nintendo - Game Boy/One.gb\"}, {\"path\": " + "\"%s/Nintendo - Game Boy Advance/Same.gba\"}]}", + content, content); + write_text(path, text); + snprintf(path, sizeof(path), "%s/builtin/content_history.lpl", playlists); + snprintf(text, sizeof(text), "{\"items\": [{\"path\": \"%s/MAME/game.zip#game.bin\"}]}", + content); + write_text(path, text); + + /* Renamed (ps2, mame), merged (gb), left where a name is taken (gba's), and a + * folder that is not a name of RetroArch's untouched (PS1). */ + assert(ps5_library_rename_system_folders(content, playlists, config) == 3); + const char *there[] = { + "ps2/Disc.iso", "gb/One.gb", "gb/Two.gb", "Nintendo - Game Boy Advance/Same.gba", + "gba/Same.gba", "mame/game.zip", "PS1/Disc.cue"}; + for (size_t i = 0; i < sizeof(there) / sizeof(there[0]); i++) + { + snprintf(path, sizeof(path), "%s/%s", content, there[i]); + assert(stat(path, &(struct stat){0}) == 0); + } + const char *gone[] = {"Sony - PlayStation 2", "Nintendo - Game Boy", "MAME"}; + for (size_t i = 0; i < sizeof(gone) / sizeof(gone[0]); i++) + { + snprintf(path, sizeof(path), "%s/%s", content, gone[i]); + assert(stat(path, &(struct stat){0}) != 0); + } + /* The playlists follow; a folder whose name only begins the same is left. */ + snprintf(path, sizeof(path), "%s/Sony - PlayStation 2.lpl", playlists); + read_back(path, text, sizeof(text)); + snprintf(want, sizeof(want), "\"scan_content_dir\": \"%s/ps2\"", content); + assert(strstr(text, want)); + snprintf(want, sizeof(want), "\"%s/ps2/Disc.iso\"", content); + assert(strstr(text, want) && !strstr(text, "PlayStation 2/")); + snprintf(path, sizeof(path), "%s/Nintendo - Game Boy.lpl", playlists); + read_back(path, text, sizeof(text)); + snprintf(want, sizeof(want), "\"%s/gb/One.gb\"", content); + assert(strstr(text, want)); + snprintf(want, sizeof(want), "\"%s/Nintendo - Game Boy Advance/Same.gba\"", content); + assert(strstr(text, want)); + snprintf(path, sizeof(path), "%s/builtin/content_history.lpl", playlists); + read_back(path, text, sizeof(text)); + snprintf(want, sizeof(want), "\"%s/mame/game.zip#game.bin\"", content); + assert(strstr(text, want)); + /* The per-folder settings follow; a core named as the folder keeps its own. */ + const char *settings[] = {"PS5SX2/ps2.cfg", "PS5SX2/ps2.opt", "remaps/PS5SX2/ps2.rmp", + "MAME/MAME.cfg", "MAME/MAME.slangp"}; + for (size_t i = 0; i < sizeof(settings) / sizeof(settings[0]); i++) + { + snprintf(path, sizeof(path), "%s/%s", config, settings[i]); + assert(stat(path, &(struct stat){0}) == 0); + } + snprintf(path, sizeof(path), "%s/PS5SX2/Sony - PlayStation 2.cfg", config); + assert(stat(path, &(struct stat){0}) != 0); + snprintf(path, sizeof(path), "%s/MAME/mame.cfg", config); + assert(stat(path, &(struct stat){0}) != 0); + assert(ps5_library_rename_system_folders(content, playlists, config) == 0); /* once */ + snprintf(path, sizeof(path), "%s/none", root); + assert(ps5_library_rename_system_folders(path, playlists, config) == -1); + } puts("ps5_library: platforms, playlists, systems, cores, folders, labels, favourites, history, " - "runtime logs, game mode commands, content folders and system folders PASS"); + "runtime logs, game mode commands, content folders, system folders and their ES-DE " + "names PASS"); return 0; } diff --git a/tools/build-title.sh b/tools/build-title.sh index 3f86083..96a1cb7 100755 --- a/tools/build-title.sh +++ b/tools/build-title.sh @@ -407,10 +407,10 @@ for core_name in "${core_names[@]}"; do cp -- "$root/build/cores/stage/info/${core_name}_libretro.info" "$dist/cores/" done -# A folder in content/ for each system the staged cores run, named as RetroArch names -# it ("Nintendo - Nintendo Entertainment System"), so a fresh install has a place for -# every system's games (issue 33). The title makes the same ones at each start -# (src/ps5_library.c, ps5_library_make_system_folders): this is that code, on the host. +# A folder in content/ for each system the staged cores run, named as ES-DE names it +# ("nes", "ps2"), so a fresh install has a place for every system's games (issue 33). The +# title makes the same ones at each start (src/ps5_library.c, +# ps5_library_make_system_folders): this is that code, on the host. cc -std=c11 -O2 -Wall -Wextra -Werror -I"$root/src" "$root/tools/system-folders.c" \ "$root/src/ps5_library.c" -o "$root/build/system-folders" rm -rf -- "$dist/content" diff --git a/webui/add-game.js b/webui/add-game.js index a6d8576..9b994f6 100644 --- a/webui/add-game.js +++ b/webui/add-game.js @@ -34,7 +34,7 @@ function addDestination() { const system = addGame.systems.find(s => s.id === $('#add-system').value); $('#add-system-icon').replaceChildren(systemIcon(system?.id)); $('#add-file').accept = system ? [...new Set(system.extensions.split('|'))].map(x => '.' + x).join(',') : ''; - $('#add-destination').textContent = system && addGame.file ? 'Destination: /app0/content/' + system.name + '/' + addGame.file.name : 'Select the system this game belongs to.'; + $('#add-destination').textContent = system && addGame.file ? 'Destination: /app0/content/' + system.id + '/' + addGame.file.name : 'Select the system this game belongs to.'; } async function openAddGame() { if (addGame) return;