From 8a51919f31f1a377efb8d8a09ee807e558531fec Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sun, 5 Sep 2021 17:39:11 +0200 Subject: [PATCH] Added the ability to make complementary game systems customizations. --- es-app/src/CollectionSystemsManager.cpp | 42 +- es-app/src/SystemData.cpp | 677 +++++++++++++----------- es-app/src/SystemData.h | 4 +- 3 files changed, 401 insertions(+), 322 deletions(-) diff --git a/es-app/src/CollectionSystemsManager.cpp b/es-app/src/CollectionSystemsManager.cpp index 684d609ee..54ca1e33a 100644 --- a/es-app/src/CollectionSystemsManager.cpp +++ b/es-app/src/CollectionSystemsManager.cpp @@ -1320,33 +1320,41 @@ void CollectionSystemsManager::addEnabledCollectionsToDisplayedSystems( std::vector CollectionSystemsManager::getSystemsFromConfig() { std::vector systems; - std::string path = SystemData::getConfigPath(false); + std::vector configPaths = SystemData::getConfigPath(false); - if (!Utils::FileSystem::exists(path)) - return systems; + // Here we don't honor the tag which may be present in the custom es_systems.xml + // file under ~/.emulationstation/custom_systems as we really want to include all the themes + // supported by ES-DE. Otherwise a user may accidentally create a custom collection that + // corresponds to a supported theme. + for (auto path : configPaths) { + if (!Utils::FileSystem::exists(path)) + return systems; - pugi::xml_document doc; + pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str()); + pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str()); #else - pugi::xml_parse_result res = doc.load_file(path.c_str()); + pugi::xml_parse_result res = doc.load_file(path.c_str()); #endif - if (!res) - return systems; + if (!res) + return systems; - // Actually read the file. - pugi::xml_node systemList = doc.child("systemList"); + // Actually read the file. + pugi::xml_node systemList = doc.child("systemList"); - if (!systemList) - return systems; + if (!systemList) + return systems; - for (pugi::xml_node system = systemList.child("system"); system; - system = system.next_sibling("system")) { - // Theme folder. - std::string themeFolder = system.child("theme").text().get(); - systems.push_back(themeFolder); + for (pugi::xml_node system = systemList.child("system"); system; + system = system.next_sibling("system")) { + // Theme folder. + std::string themeFolder = system.child("theme").text().get(); + if (std::find(systems.cbegin(), systems.cend(), themeFolder) == systems.cend()) + systems.push_back(themeFolder); + } } + std::sort(systems.begin(), systems.end()); return systems; } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 431ebbaac..b91ed7e73 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -387,219 +387,245 @@ bool SystemData::loadConfig() LOG(LogInfo) << "Populating game systems..."; - std::string path = getConfigPath(true); + std::vector configPaths = getConfigPath(true); const std::string rompath = FileData::getROMDirectory(); - LOG(LogInfo) << "Parsing systems configuration file \"" << path << "\"..."; + bool onlyProcessCustomFile = false; - pugi::xml_document doc; + for (auto path : configPaths) { + // If the loadExclusive tag is present in the custom es_systems.xml file, then skip + // processing of the bundled configuration file. + if (onlyProcessCustomFile) + break; + + LOG(LogInfo) << "Parsing systems configuration file \"" << path << "\"..."; + + pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str()); + pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str()); #else - pugi::xml_parse_result res = doc.load_file(path.c_str()); + pugi::xml_parse_result res = doc.load_file(path.c_str()); #endif - if (!res) { - LOG(LogError) << "Couldn't parse es_systems.xml: " << res.description(); - return true; - } + if (!res) { + LOG(LogError) << "Couldn't parse es_systems.xml: " << res.description(); + return true; + } - // Actually read the file. - pugi::xml_node systemList = doc.child("systemList"); - - if (!systemList) { - LOG(LogError) << "es_systems.xml is missing the tag"; - return true; - } - - for (pugi::xml_node system = systemList.child("system"); system; - system = system.next_sibling("system")) { - std::string name; - std::string fullname; - std::string path; - std::string themeFolder; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - - auto nameFindFunc = [&] { - for (auto system : sSystemVector) { - if (system->mName == name) { - LOG(LogWarning) << "A system with the name \"" << name - << "\" has already been loaded, skipping duplicate entry"; - return true; - } + pugi::xml_node loadExclusive = doc.child("loadExclusive"); + if (loadExclusive) { + if (path == configPaths.front() && configPaths.size() > 1) { + LOG(LogInfo) << "Only loading custom file as the tag is present"; + onlyProcessCustomFile = true; } - return false; - }; + else { + LOG(LogWarning) << "A tag is present in the bundled es_systems.xml " + "file, ignoring it as this is only supposed to be used for the " + "custom es_systems.xml file"; + } + } - // If the name is matching a system that has already been loaded, then skip the entry. - if (nameFindFunc()) - continue; + // Actually read the file. + pugi::xml_node systemList = doc.child("systemList"); - // If there is a %ROMPATH% variable set for the system, expand it. By doing this - // it's possible to use either absolute ROM paths in es_systems.xml or to utilize - // the ROM path configured as ROMDirectory in es_settings.xml. If it's set to "" - // in this configuration file, the default hardcoded path $HOME/ROMs/ will be used. - path = Utils::String::replace(path, "%ROMPATH%", rompath); + if (!systemList) { + LOG(LogError) << "es_systems.xml is missing the tag"; + return true; + } + + for (pugi::xml_node system = systemList.child("system"); system; + system = system.next_sibling("system")) { + std::string name; + std::string fullname; + std::string path; + std::string themeFolder; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + + auto nameFindFunc = [&] { + for (auto system : sSystemVector) { + if (system->mName == name) { + LOG(LogWarning) << "A system with the name \"" << name + << "\" has already been loaded, skipping duplicate entry"; + return true; + } + } + return false; + }; + + // If the name is matching a system that has already been loaded, then skip the entry. + if (nameFindFunc()) + continue; + + // If there is a %ROMPATH% variable set for the system, expand it. By doing this + // it's possible to use either absolute ROM paths in es_systems.xml or to utilize + // the ROM path configured as ROMDirectory in es_settings.xml. If it's set to "" + // in this configuration file, the default hardcoded path $HOME/ROMs/ will be used. + path = Utils::String::replace(path, "%ROMPATH%", rompath); #if defined(_WIN64) - path = Utils::String::replace(path, "\\", "/"); + path = Utils::String::replace(path, "\\", "/"); #endif - path = Utils::String::replace(path, "//", "/"); + path = Utils::String::replace(path, "//", "/"); - // Check that the ROM directory for the system is valid or otherwise abort the processing. - if (!Utils::FileSystem::exists(path)) { - LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name - << "\" as the defined ROM directory \"" << path << "\" does not exist"; - continue; - } - if (!Utils::FileSystem::isDirectory(path)) { - LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name - << "\" as the defined ROM directory \"" << path - << "\" is not actually a directory"; - continue; - } - if (Utils::FileSystem::isSymlink(path)) { - // Make sure that the symlink is not pointing to somewhere higher in the hierarchy - // as that would lead to an infite loop, meaning the application would never start. - std::string resolvedRompath = Utils::FileSystem::getCanonicalPath(rompath); - if (resolvedRompath.find(Utils::FileSystem::getCanonicalPath(path)) == 0) { - LOG(LogWarning) << "Skipping system \"" << name - << "\" as the defined ROM directory \"" << path - << "\" is an infinitely recursive symlink"; + // Check that the ROM directory for the system is valid or otherwise abort the + // processing. + if (!Utils::FileSystem::exists(path)) { + LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name + << "\" as the defined ROM directory \"" << path + << "\" does not exist"; continue; } - } + if (!Utils::FileSystem::isDirectory(path)) { + LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name + << "\" as the defined ROM directory \"" << path + << "\" is not actually a directory"; + continue; + } + if (Utils::FileSystem::isSymlink(path)) { + // Make sure that the symlink is not pointing to somewhere higher in the hierarchy + // as that would lead to an infite loop, meaning the application would never start. + std::string resolvedRompath = Utils::FileSystem::getCanonicalPath(rompath); + if (resolvedRompath.find(Utils::FileSystem::getCanonicalPath(path)) == 0) { + LOG(LogWarning) + << "Skipping system \"" << name << "\" as the defined ROM directory \"" + << path << "\" is an infinitely recursive symlink"; + continue; + } + } - // Convert extensions list from a string into a vector of strings. - std::vector extensions = readList(system.child("extension").text().get()); + // Convert extensions list from a string into a vector of strings. + std::vector extensions = readList(system.child("extension").text().get()); - // Load all launch command tags for the system and if there are multiple tags, then - // the label attribute needs to be set on all entries as it's a requirement for the - // alternative emulator logic. - std::vector> commands; - for (pugi::xml_node entry = system.child("command"); entry; - entry = entry.next_sibling("command")) { - if (!entry.attribute("label")) { - if (commands.size() == 1) { - // The first command tag had a label but the second one doesn't. + // Load all launch command tags for the system and if there are multiple tags, then + // the label attribute needs to be set on all entries as it's a requirement for the + // alternative emulator logic. + std::vector> commands; + for (pugi::xml_node entry = system.child("command"); entry; + entry = entry.next_sibling("command")) { + if (!entry.attribute("label")) { + if (commands.size() == 1) { + // The first command tag had a label but the second one doesn't. + LOG(LogError) + << "Missing mandatory label attribute for alternative emulator " + "entry, only the first command tag will be processed for system \"" + << name << "\""; + break; + } + else if (commands.size() > 1) { + // At least two command tags had a label but this one doesn't. + LOG(LogError) + << "Missing mandatory label attribute for alternative emulator " + "entry, no additional command tags will be processed for system \"" + << name << "\""; + break; + } + } + else if (!commands.empty() && commands.back().second == "") { + // There are more than one command tags and the first tag did not have a label. LOG(LogError) << "Missing mandatory label attribute for alternative emulator " "entry, only the first command tag will be processed for system \"" << name << "\""; break; } - else if (commands.size() > 1) { - // At least two command tags had a label but this one doesn't. - LOG(LogError) - << "Missing mandatory label attribute for alternative emulator " - "entry, no additional command tags will be processed for system \"" - << name << "\""; + commands.push_back( + std::make_pair(entry.text().get(), entry.attribute("label").as_string())); + } + + // Platform ID list + const std::string platformList = + Utils::String::toLower(system.child("platform").text().get()); + + if (platformList == "") { + LOG(LogWarning) << "No platform defined for system \"" << name + << "\", scraper searches will be inaccurate"; + } + + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) { + std::string str = *it; + PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str); + + if (platformId == PlatformIds::PLATFORM_IGNORE) { + // When platform is PLATFORM_IGNORE, do not allow other platforms. + platformIds.clear(); + platformIds.push_back(platformId); break; } - } - else if (!commands.empty() && commands.back().second == "") { - // There are more than one command tags and the first tag did not have a label. - LOG(LogError) << "Missing mandatory label attribute for alternative emulator " - "entry, only the first command tag will be processed for system \"" - << name << "\""; - break; - } - commands.push_back( - std::make_pair(entry.text().get(), entry.attribute("label").as_string())); - } - // Platform ID list - const std::string platformList = - Utils::String::toLower(system.child("platform").text().get()); - - if (platformList == "") { - LOG(LogWarning) << "No platform defined for system \"" << name - << "\", scraper searches will be inaccurate"; - } - - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) { - std::string str = *it; - PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str); - - if (platformId == PlatformIds::PLATFORM_IGNORE) { - // When platform is PLATFORM_IGNORE, do not allow other platforms. - platformIds.clear(); - platformIds.push_back(platformId); - break; + // If there's a platform entry defined but it does not match the list of supported + // platforms, then generate a warning. + if (str != "" && platformId == PlatformIds::PLATFORM_UNKNOWN) + LOG(LogWarning) << "Unknown platform \"" << str << "\" defined for system \"" + << name << "\", scraper searches will be inaccurate"; + else if (platformId != PlatformIds::PLATFORM_UNKNOWN) + platformIds.push_back(platformId); } - // If there's a platform entry defined but it does not match the list of supported - // platforms, then generate a warning. - if (str != "" && platformId == PlatformIds::PLATFORM_UNKNOWN) - LOG(LogWarning) << "Unknown platform \"" << str << "\" defined for system \"" - << name << "\", scraper searches will be inaccurate"; - else if (platformId != PlatformIds::PLATFORM_UNKNOWN) - platformIds.push_back(platformId); - } + // Theme folder. + themeFolder = system.child("theme").text().as_string(name.c_str()); - // Theme folder. - themeFolder = system.child("theme").text().as_string(name.c_str()); + // Validate. - // Validate. + if (name.empty()) { + LOG(LogError) + << "A system in the es_systems.xml file has no name defined, skipping entry"; + continue; + } + else if (fullname.empty() || path.empty() || extensions.empty() || commands.empty()) { + LOG(LogError) << "System \"" << name + << "\" is missing the fullname, path, " + "extension, or command tag, skipping entry"; + continue; + } - if (name.empty()) { - LOG(LogError) - << "A system in the es_systems.xml file has no name defined, skipping entry"; - continue; - } - else if (fullname.empty() || path.empty() || extensions.empty() || commands.empty()) { - LOG(LogError) << "System \"" << name - << "\" is missing the fullname, path, " - "extension, or command tag, skipping entry"; - continue; - } - - // Convert path to generic directory seperators. - path = Utils::FileSystem::getGenericPath(path); + // Convert path to generic directory seperators. + path = Utils::FileSystem::getGenericPath(path); #if defined(_WIN64) - if (!Settings::getInstance()->getBool("ShowHiddenFiles") && - Utils::FileSystem::isHidden(path)) { - LOG(LogWarning) << "Skipping hidden ROM folder \"" << path << "\""; - continue; - } + if (!Settings::getInstance()->getBool("ShowHiddenFiles") && + Utils::FileSystem::isHidden(path)) { + LOG(LogWarning) << "Skipping hidden ROM folder \"" << path << "\""; + continue; + } #endif - // Create the system runtime environment data. - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommands = commands; - envData->mPlatformIds = platformIds; + // Create the system runtime environment data. + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommands = commands; + envData->mPlatformIds = platformIds; - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - bool onlyHidden = false; + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + bool onlyHidden = false; - // If the option to show hidden games has been disabled, then check whether all - // games for the system are hidden. That will flag the system as empty. - if (!Settings::getInstance()->getBool("ShowHiddenGames")) { - std::vector recursiveGames = newSys->getRootFolder()->getChildrenRecursive(); - onlyHidden = true; - for (auto it = recursiveGames.cbegin(); it != recursiveGames.cend(); it++) { - if ((*it)->getType() != FOLDER) { - onlyHidden = (*it)->getHidden(); - if (!onlyHidden) - break; + // If the option to show hidden games has been disabled, then check whether all + // games for the system are hidden. That will flag the system as empty. + if (!Settings::getInstance()->getBool("ShowHiddenGames")) { + std::vector recursiveGames = + newSys->getRootFolder()->getChildrenRecursive(); + onlyHidden = true; + for (auto it = recursiveGames.cbegin(); it != recursiveGames.cend(); it++) { + if ((*it)->getType() != FOLDER) { + onlyHidden = (*it)->getHidden(); + if (!onlyHidden) + break; + } } } - } - if (newSys->getRootFolder()->getChildrenByFilename().size() == 0 || onlyHidden) { - LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name - << "\" as no files matched any of the defined file extensions"; - delete newSys; - } - else { - sSystemVector.push_back(newSys); + if (newSys->getRootFolder()->getChildrenByFilename().size() == 0 || onlyHidden) { + LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name + << "\" as no files matched any of the defined file extensions"; + delete newSys; + } + else { + sSystemVector.push_back(newSys); + } } } @@ -634,8 +660,10 @@ void SystemData::deleteSystems() sSystemVector.clear(); } -std::string SystemData::getConfigPath(bool legacyWarning) +std::vector SystemData::getConfigPath(bool legacyWarning) { + std::vector paths; + if (legacyWarning) { std::string legacyConfigFile = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; @@ -662,7 +690,7 @@ std::string SystemData::getConfigPath(bool legacyWarning) if (Utils::FileSystem::exists(path)) { LOG(LogInfo) << "Found custom systems configuration file"; - return path; + paths.push_back(path); } #if defined(_WIN64) @@ -674,18 +702,16 @@ std::string SystemData::getConfigPath(bool legacyWarning) path = ResourceManager::getInstance()->getResourcePath(":/systems/unix/es_systems.xml", true); #endif - return path; + paths.push_back(path); + return paths; } bool SystemData::createSystemDirectories() { - std::string path = getConfigPath(false); + std::vector configPaths = getConfigPath(true); const std::string rompath = FileData::getROMDirectory(); - if (!Utils::FileSystem::exists(path)) { - LOG(LogInfo) << "Systems configuration file does not exist, aborting"; - return true; - } + bool onlyProcessCustomFile = false; LOG(LogInfo) << "Generating ROM directory structure..."; @@ -706,144 +732,187 @@ bool SystemData::createSystemDirectories() LOG(LogInfo) << "Base ROM directory \"" << rompath << "\" already exists"; } - LOG(LogInfo) << "Parsing systems configuration file \"" << path << "\"..."; - - pugi::xml_document doc; + if (configPaths.size() > 1) { + // If the loadExclusive tag is present in the custom es_systems.xml file, then skip + // processing of the bundled configuration file. + pugi::xml_document doc; #if defined(_WIN64) - pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str()); + pugi::xml_parse_result res = + doc.load_file(Utils::String::stringToWideString(configPath).c_str()); #else - pugi::xml_parse_result res = doc.load_file(path.c_str()); + pugi::xml_parse_result res = doc.load_file(configPaths.front().c_str()); #endif - - if (!res) { - LOG(LogError) << "Couldn't parse es_systems.xml"; - LOG(LogError) << res.description(); - return true; - } - - // Actually read the file. - pugi::xml_node systemList = doc.child("systemList"); - - if (!systemList) { - LOG(LogError) << "es_systems.xml is missing the tag"; - return true; - } - - std::vector systemsVector; - - for (pugi::xml_node system = systemList.child("system"); system; - system = system.next_sibling("system")) { - std::string systemDir; - std::string name; - std::string fullname; - std::string path; - std::string extensions; - std::vector commands; - std::string platform; - std::string themeFolder; - const std::string systemInfoFileName = "/systeminfo.txt"; - bool replaceInfoFile = false; - std::ofstream systemInfoFile; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - extensions = system.child("extension").text().get(); - for (pugi::xml_node entry = system.child("command"); entry; - entry = entry.next_sibling("command")) { - commands.push_back(entry.text().get()); + if (res) { + pugi::xml_node loadExclusive = doc.child("loadExclusive"); + if (loadExclusive) + onlyProcessCustomFile = true; } - platform = Utils::String::toLower(system.child("platform").text().get()); - themeFolder = system.child("theme").text().as_string(name.c_str()); + } - // Check that the %ROMPATH% variable is actually used for the path element. - // If not, skip the system. - if (path.find("%ROMPATH%") != 0) { - LOG(LogWarning) << "The path element for system \"" << name - << "\" does not " - "utilize the %ROMPATH% variable, skipping entry"; + // Process the custom es_systems.xml file after the bundled file, as any systems with identical + // tags will be overwritten by the last occurrence. + std::reverse(configPaths.begin(), configPaths.end()); + + std::vector> systemsVector; + + for (auto configPath : configPaths) { + // If the loadExclusive tag is present. + if (onlyProcessCustomFile && configPath == configPaths.front()) continue; - } - else { - systemDir = path.substr(9, path.size() - 9); - } - // Trim any leading directory separator characters. - systemDir.erase(systemDir.begin(), - std::find_if(systemDir.begin(), systemDir.end(), - [](char c) { return c != '/' && c != '\\'; })); - - if (!Utils::FileSystem::exists(rompath + systemDir)) { - if (!Utils::FileSystem::createDirectory(rompath + systemDir)) { - LOG(LogError) << "Couldn't create system directory \"" << systemDir - << "\", permission problems or disk full?"; - return true; - } - else { - LOG(LogInfo) << "Created system directory \"" << systemDir << "\""; - } - } - else { - LOG(LogInfo) << "System directory \"" << systemDir << "\" already exists"; - } - - if (Utils::FileSystem::exists(rompath + systemDir + systemInfoFileName)) - replaceInfoFile = true; - else - replaceInfoFile = false; - - if (replaceInfoFile) { - if (Utils::FileSystem::removeFile(rompath + systemDir + systemInfoFileName)) - return true; - } + LOG(LogInfo) << "Parsing systems configuration file \"" << configPath << "\"..."; + pugi::xml_document doc; #if defined(_WIN64) - systemInfoFile.open( - Utils::String::stringToWideString(rompath + systemDir + systemInfoFileName).c_str()); + pugi::xml_parse_result res = + doc.load_file(Utils::String::stringToWideString(configPath).c_str()); #else - systemInfoFile.open(rompath + systemDir + systemInfoFileName); + pugi::xml_parse_result res = doc.load_file(configPath.c_str()); #endif - if (systemInfoFile.fail()) { - LOG(LogError) << "Couldn't create system information file \"" - << rompath + systemDir + systemInfoFileName - << "\", permission problems or disk full?"; - systemInfoFile.close(); + if (!res) { + LOG(LogError) << "Couldn't parse es_systems.xml"; + LOG(LogError) << res.description(); return true; } - systemInfoFile << "System name:" << std::endl; - systemInfoFile << name << std::endl << std::endl; - systemInfoFile << "Full system name:" << std::endl; - systemInfoFile << fullname << std::endl << std::endl; - systemInfoFile << "Supported file extensions:" << std::endl; - systemInfoFile << extensions << std::endl << std::endl; - systemInfoFile << "Launch command:" << std::endl; - systemInfoFile << commands.front() << std::endl << std::endl; - // Alternative emulator configuration entries. - if (commands.size() > 1) { - systemInfoFile << (commands.size() == 2 ? "Alternative launch command:" : - "Alternative launch commands:") - << std::endl; - for (auto it = commands.cbegin() + 1; it != commands.cend(); it++) - systemInfoFile << (*it) << std::endl; - systemInfoFile << std::endl; - } - systemInfoFile << "Platform (for scraping):" << std::endl; - systemInfoFile << platform << std::endl << std::endl; - systemInfoFile << "Theme folder:" << std::endl; - systemInfoFile << themeFolder << std::endl; - systemInfoFile.close(); + // Actually read the file. + pugi::xml_node systemList = doc.child("systemList"); - systemsVector.push_back(systemDir + ": " + fullname); - - if (replaceInfoFile) { - LOG(LogInfo) << "Replaced existing system information file \"" - << rompath + systemDir + systemInfoFileName << "\""; + if (!systemList) { + LOG(LogError) << "es_systems.xml is missing the tag"; + return true; } - else { - LOG(LogInfo) << "Created system information file \"" - << rompath + systemDir + systemInfoFileName << "\""; + + for (pugi::xml_node system = systemList.child("system"); system; + system = system.next_sibling("system")) { + std::string systemDir; + std::string name; + std::string fullname; + std::string path; + std::string extensions; + std::vector commands; + std::string platform; + std::string themeFolder; + const std::string systemInfoFileName = "/systeminfo.txt"; + bool replaceInfoFile = false; + std::ofstream systemInfoFile; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + extensions = system.child("extension").text().get(); + for (pugi::xml_node entry = system.child("command"); entry; + entry = entry.next_sibling("command")) { + commands.push_back(entry.text().get()); + } + platform = Utils::String::toLower(system.child("platform").text().get()); + themeFolder = system.child("theme").text().as_string(name.c_str()); + + // Check that the %ROMPATH% variable is actually used for the path element. + // If not, skip the system. + if (path.find("%ROMPATH%") != 0) { + LOG(LogWarning) << "The path element for system \"" << name + << "\" does not " + "utilize the %ROMPATH% variable, skipping entry"; + continue; + } + else { + systemDir = path.substr(9, path.size() - 9); + } + + // Trim any leading directory separator characters. + systemDir.erase(systemDir.begin(), + std::find_if(systemDir.begin(), systemDir.end(), + [](char c) { return c != '/' && c != '\\'; })); + + if (!Utils::FileSystem::exists(rompath + systemDir)) { + if (!Utils::FileSystem::createDirectory(rompath + systemDir)) { + LOG(LogError) << "Couldn't create system directory \"" << systemDir + << "\", permission problems or disk full?"; + return true; + } + else { + LOG(LogInfo) << "Created system directory \"" << systemDir << "\""; + } + } + else { + LOG(LogInfo) << "System directory \"" << systemDir << "\" already exists"; + } + + if (Utils::FileSystem::exists(rompath + systemDir + systemInfoFileName)) + replaceInfoFile = true; + else + replaceInfoFile = false; + + if (replaceInfoFile) { + if (Utils::FileSystem::removeFile(rompath + systemDir + systemInfoFileName)) + return true; + } + +#if defined(_WIN64) + systemInfoFile.open( + Utils::String::stringToWideString(rompath + systemDir + systemInfoFileName) + .c_str()); +#else + systemInfoFile.open(rompath + systemDir + systemInfoFileName); +#endif + + if (systemInfoFile.fail()) { + LOG(LogError) << "Couldn't create system information file \"" + << rompath + systemDir + systemInfoFileName + << "\", permission problems or disk full?"; + systemInfoFile.close(); + return true; + } + + systemInfoFile << "System name:" << std::endl; + if (configPaths.size() != 1 && configPath == configPaths.back()) + systemInfoFile << name << " (custom system)" << std::endl << std::endl; + else + systemInfoFile << name << std::endl << std::endl; + systemInfoFile << "Full system name:" << std::endl; + systemInfoFile << fullname << std::endl << std::endl; + systemInfoFile << "Supported file extensions:" << std::endl; + systemInfoFile << extensions << std::endl << std::endl; + systemInfoFile << "Launch command:" << std::endl; + systemInfoFile << commands.front() << std::endl << std::endl; + // Alternative emulator configuration entries. + if (commands.size() > 1) { + systemInfoFile << (commands.size() == 2 ? "Alternative launch command:" : + "Alternative launch commands:") + << std::endl; + for (auto it = commands.cbegin() + 1; it != commands.cend(); it++) + systemInfoFile << (*it) << std::endl; + systemInfoFile << std::endl; + } + systemInfoFile << "Platform (for scraping):" << std::endl; + systemInfoFile << platform << std::endl << std::endl; + systemInfoFile << "Theme folder:" << std::endl; + systemInfoFile << themeFolder << std::endl; + systemInfoFile.close(); + + auto systemIter = std::find_if(systemsVector.cbegin(), systemsVector.cend(), + [systemDir](std::pair system) { + return system.first == systemDir; + }); + + if (systemIter != systemsVector.cend()) + systemsVector.erase(systemIter); + + if (configPaths.size() != 1 && configPath == configPaths.back()) + systemsVector.push_back(std::make_pair(systemDir + " (custom system)", fullname)); + else + systemsVector.push_back(std::make_pair(systemDir, fullname)); + + if (replaceInfoFile) { + LOG(LogInfo) << "Replaced existing system information file \"" + << rompath + systemDir + systemInfoFileName << "\""; + } + else { + LOG(LogInfo) << "Created system information file \"" + << rompath + systemDir + systemInfoFileName << "\""; + } } } @@ -870,8 +939,10 @@ bool SystemData::createSystemDirectories() systemsFileSuccess = false; } else { - for (std::string systemEntry : systemsVector) { - systemsFile << systemEntry << std::endl; + std::sort(systemsVector.begin(), systemsVector.end()); + for (auto systemEntry : systemsVector) { + systemsFile << systemEntry.first.append(": ").append(systemEntry.second) + << std::endl; } systemsFile.close(); } diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 0b8c7b17c..63691ed92 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -102,9 +102,9 @@ public: std::string getLaunchCommandFromLabel(const std::string& label); static void deleteSystems(); - // Loads the systems configuration file at getConfigPath() and creates the systems. + // Loads the systems configuration file(s) at getConfigPath() and creates the systems. static bool loadConfig(); - static std::string getConfigPath(bool legacyWarning); + static std::vector getConfigPath(bool legacyWarning); // Generates the game system directories and information files based on es_systems.xml. static bool createSystemDirectories();