// SPDX-License-Identifier: MIT // // ES-DE Frontend // SystemData.cpp // // Provides data structures for the game systems and populates and indexes them based // on the configuration in es_systems.xml as well as the presence of game ROM files. // Also provides functions to read and write to the gamelist files and to handle theme // loading. // #include "SystemData.h" #include "CollectionSystemsManager.h" #include "FileFilterIndex.h" #include "FileSorts.h" #include "GamelistFileParser.h" #include "InputManager.h" #include "Log.h" #include "Settings.h" #include "ThemeData.h" #include "UIModeController.h" #include "resources/ResourceManager.h" #include "utils/FileSystemUtil.h" #include "utils/LocalizationUtil.h" #include "utils/StringUtil.h" #include "views/GamelistView.h" #include "views/ViewController.h" #include #include #include #include #include FindRules::FindRules() { LOG(LogInfo) << "Loading emulator find rules..."; loadFindRules(); } void FindRules::loadFindRules() { std::vector paths; std::string filePath {Utils::FileSystem::getAppDataDirectory() + "/custom_systems/es_find_rules.xml"}; if (Utils::FileSystem::exists(filePath)) { paths.emplace_back(filePath); LOG(LogInfo) << "Found custom find rules configuration file"; } #if defined(__ANDROID__) filePath = ResourceManager::getInstance().getResourcePath(":/systems/android/es_find_rules.xml", false); #elif defined(__linux__) filePath = ResourceManager::getInstance().getResourcePath(":/systems/linux/es_find_rules.xml", false); #elif defined(_WIN64) filePath = ResourceManager::getInstance().getResourcePath(":/systems/windows/es_find_rules.xml", false); #elif defined(__APPLE__) filePath = ResourceManager::getInstance().getResourcePath(":/systems/macos/es_find_rules.xml", false); #elif defined(__HAIKU__) filePath = ResourceManager::getInstance().getResourcePath(":/systems/haiku/es_find_rules.xml", false); #else filePath = ResourceManager::getInstance().getResourcePath(":/systems/unix/es_find_rules.xml", false); #endif if (filePath.empty() && paths.empty()) { LOG(LogWarning) << "No find rules configuration file found"; return; } if (!filePath.empty()) paths.emplace_back(filePath); for (auto& path : paths) { #if defined(_WIN64) LOG(LogInfo) << "Parsing find rules configuration file \"" << Utils::String::replace(path, "/", "\\") << "\"..."; #else LOG(LogInfo) << "Parsing find rules configuration file \"" << path << "\"..."; #endif pugi::xml_document doc; #if defined(_WIN64) const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(path).c_str())}; #else const pugi::xml_parse_result& res {doc.load_file(path.c_str())}; #endif if (!res) { LOG(LogError) << "Couldn't parse es_find_rules.xml: " << res.description(); continue; } // Actually read the file. const pugi::xml_node& ruleList {doc.child("ruleList")}; if (!ruleList) { LOG(LogError) << "es_find_rules.xml is missing the tag"; continue; } EmulatorRules emulatorRules; CoreRules coreRules; for (pugi::xml_node emulator {ruleList.child("emulator")}; emulator; emulator = emulator.next_sibling("emulator")) { const std::string& emulatorName {emulator.attribute("name").as_string()}; if (emulatorName.empty()) { LOG(LogWarning) << "Found emulator tag without name attribute, skipping entry"; continue; } if (mEmulators.find(emulatorName) != mEmulators.end()) { if (paths.size() == 1) { LOG(LogWarning) << "Found repeating emulator tag \"" << emulatorName << "\", skipping entry"; } continue; } for (pugi::xml_node rule = emulator.child("rule"); rule; rule = rule.next_sibling("rule")) { const std::string& ruleType {rule.attribute("type").as_string()}; if (ruleType.empty()) { LOG(LogWarning) << "Found rule tag without type attribute for emulator \"" << emulatorName << "\", skipping entry"; continue; } #if defined(_WIN64) if (ruleType != "winregistrypath" && ruleType != "winregistryvalue" && ruleType != "systempath" && ruleType != "staticpath") { #elif defined(__ANDROID__) if (ruleType != "androidpackage" && ruleType != "systempath" && ruleType != "staticpath") { #else if (ruleType != "systempath" && ruleType != "staticpath") { #endif LOG(LogWarning) << "Found invalid rule type \"" << ruleType << "\" for emulator \"" << emulatorName << "\", skipping entry"; continue; } for (pugi::xml_node entry {rule.child("entry")}; entry; entry = entry.next_sibling("entry")) { const std::string& entryValue {entry.text().get()}; if (ruleType == "systempath") emulatorRules.systemPaths.emplace_back(entryValue); else if (ruleType == "staticpath") emulatorRules.staticPaths.emplace_back(entryValue); #if defined(_WIN64) else if (ruleType == "winregistrypath") emulatorRules.winRegistryPaths.emplace_back(entryValue); else if (ruleType == "winregistryvalue") emulatorRules.winRegistryValues.emplace_back(entryValue); #elif defined(__ANDROID__) else if (ruleType == "androidpackage") emulatorRules.androidPackages.emplace_back(entryValue); #endif } } mEmulators[emulatorName] = emulatorRules; emulatorRules.systemPaths.clear(); emulatorRules.staticPaths.clear(); #if defined(_WIN64) emulatorRules.winRegistryPaths.clear(); emulatorRules.winRegistryValues.clear(); #elif defined(__ANDROID__) emulatorRules.androidPackages.clear(); #endif } for (pugi::xml_node core {ruleList.child("core")}; core; core = core.next_sibling("core")) { const std::string& coreName {core.attribute("name").as_string()}; if (coreName.empty()) { LOG(LogWarning) << "Found core tag without name attribute, skipping entry"; continue; } if (mCores.find(coreName) != mCores.end()) { if (paths.size() == 1) { LOG(LogWarning) << "Found repeating core tag \"" << coreName << "\", skipping entry"; } continue; } for (pugi::xml_node rule {core.child("rule")}; rule; rule = rule.next_sibling("rule")) { const std::string& ruleType {rule.attribute("type").as_string()}; if (ruleType.empty()) { LOG(LogWarning) << "Found rule tag without type attribute for core \"" << coreName << "\", skipping entry"; continue; } if (ruleType != "corepath") { LOG(LogWarning) << "Found invalid rule type \"" << ruleType << "\" for core \"" << coreName << "\", skipping entry"; continue; } for (pugi::xml_node entry {rule.child("entry")}; entry; entry = entry.next_sibling("entry")) { const std::string& entryValue {entry.text().get()}; if (ruleType == "corepath") coreRules.corePaths.emplace_back(entryValue); } } mCores[coreName] = coreRules; coreRules.corePaths.clear(); } } } SystemData::SystemData(const std::string& name, const std::string& fullName, const std::string& sortName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem, bool CustomCollectionSystem) : mName {name} , mFullName {fullName} , mSortName {sortName} , mEnvData {envData} , mThemeFolder {themeFolder} , mSymlinkMaxDepthReached {false} , mIsCollectionSystem {CollectionSystem} , mIsCustomCollectionSystem {CustomCollectionSystem} , mIsGroupedCustomCollectionSystem {false} , mIsGameSystem {true} , mScrapeFlag {false} , mFlattenFolders {false} , mPlaceholder {nullptr} { mFilterIndex = new FileFilterIndex(); // If it's an actual system, initialize it, if not, just create the data structure. if (!CollectionSystem) { mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); mRootFolder->metadata.set("name", mFullName); if (!Settings::getInstance()->getBool("ParseGamelistOnly")) { // If there was an error populating the folder or if there were no games found, // then don't continue with any additional process steps for this system. if (!populateFolder(mRootFolder)) return; } if (!Settings::getInstance()->getBool("IgnoreGamelist")) GamelistFileParser::parseGamelist(this); setupSystemSortType(mRootFolder); mRootFolder->sort(mRootFolder->getSortTypeFromString(mRootFolder->getSortTypeString()), Settings::getInstance()->getBool("FavoritesFirst")); indexAllGameFilters(mRootFolder); } else { // Virtual systems are updated afterwards by CollectionSystemsManager. // We're just creating the data structure here. mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); setupSystemSortType(mRootFolder); } // This placeholder can be used later in the gamelist view. mPlaceholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", getSystemEnvData(), this); setIsGameSystemStatus(); loadTheme(ThemeTriggers::TriggerType::NONE); } SystemData::~SystemData() { if (Settings::getInstance()->getString("SaveGamelistsMode") == "on exit") { if (mRootFolder->getGameCount().first + mRootFolder->getGameCount().second != 0) writeMetaData(); } if (!mEnvData->mStartPath.empty()) delete mEnvData; delete mRootFolder; delete mPlaceholder; delete mFilterIndex; } void SystemData::setIsGameSystemStatus() { // Reserved for future use, could be used to exclude certain systems from some operations, // such as dedicated tools systems and similar. mIsGameSystem = true; } bool SystemData::populateFolder(FileData* folder) { if (mSymlinkMaxDepthReached) return false; std::string filePath; std::string extension; const std::string& folderPath {folder->getPath()}; const bool showHiddenFiles {Settings::getInstance()->getBool("ShowHiddenFiles")}; const Utils::FileSystem::StringList& dirContent {Utils::FileSystem::getDirContent(folderPath)}; bool isGame {false}; // If system directory exists but contains no games, return as error. if (dirContent.size() == 0) return false; if (std::find(dirContent.cbegin(), dirContent.cend(), mEnvData->mStartPath + "/noload.txt") != dirContent.cend()) { LOG(LogInfo) << "Not populating system \"" << mName << "\" as a noload.txt file is present"; return false; } if (std::find(dirContent.cbegin(), dirContent.cend(), mEnvData->mStartPath + "/flatten.txt") != dirContent.cend()) { LOG(LogInfo) << "A flatten.txt file is present for the \"" << mName << "\" system, folder flattening will be applied"; mFlattenFolders = true; } for (Utils::FileSystem::StringList::const_iterator it {dirContent.cbegin()}; it != dirContent.cend(); ++it) { filePath = *it; const bool isDirectory {Utils::FileSystem::isDirectory(filePath)}; // Skip any recursive symlinks as those would hang the application at various places. if (Utils::FileSystem::isSymlink(filePath)) { if (Utils::FileSystem::resolveSymlink(filePath) == Utils::FileSystem::getFileName(filePath)) { LOG(LogWarning) << "Skipped \"" << filePath << "\" as it's a recursive symlink"; continue; } } // Skip hidden files and folders. if (!showHiddenFiles && Utils::FileSystem::isHidden(filePath)) { LOG(LogDebug) << "SystemData::populateFolder(): Skipping hidden " << (isDirectory ? "directory \"" : "file \"") << filePath << "\""; continue; } // This is a little complicated because we allow a list // of extensions to be defined (delimited with a space). // We first get the extension of the file itself: extension = Utils::FileSystem::getExtension(filePath); isGame = false; if (std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend() && !(isDirectory && extension == ".")) { FileData* newGame {new FileData(GAME, filePath, mEnvData, this)}; if (newGame->metadata.get("name") == "") { LOG(LogWarning) << "Skipped \"" << filePath << "\" as it has no filename"; delete newGame; continue; } // If adding a configured file extension to a directory it will get interpreted as // a regular file. This is useful for displaying multi-file/multi-disc games as single // entries or for emulators that can get directories passed to them as command line // parameters instead of regular files. In these instances we remove the extension // from the metadata name so it does not show up in the gamelists and similar. if (isDirectory && extension != ".") { const std::string folderName {newGame->metadata.get("name")}; newGame->metadata.set( "name", folderName.substr(0, folderName.length() - extension.length())); } // Prevent new arcade assets from being added. if (!newGame->isArcadeAsset()) { folder->addChild(newGame); isGame = true; } else { delete newGame; } } // Add directories that also do not match an extension as folders. if (!isGame && isDirectory) { // Make sure that it's not a recursive symlink as the application would run into a // loop trying to resolve the link. if (Utils::FileSystem::isSymlink(filePath)) { bool recursiveSymlink {false}; const std::string& canonicalPath {Utils::FileSystem::getCanonicalPath(filePath)}; const std::string& canonicalStartPath { Utils::FileSystem::getCanonicalPath(mEnvData->mStartPath)}; // Last resort hack to prevent recursive symlinks in some really unusual situations. if (filePath.length() > canonicalStartPath.length() + 100) { int folderDepth {0}; const std::string& path {filePath.substr(canonicalStartPath.length())}; for (char character : path) { if (character == '/') { ++folderDepth; if (folderDepth == 20) { LOG(LogWarning) << "Skipped \"" << filePath << "\" as it seems to be a recursive symlink"; mSymlinkMaxDepthReached = true; return false; } } } } if (canonicalStartPath.find(canonicalPath) != std::string::npos) recursiveSymlink = true; else if (canonicalPath.size() >= canonicalStartPath.size() && canonicalPath.find(canonicalStartPath) != std::string::npos) { const std::string& combinedPath { mEnvData->mStartPath + canonicalPath.substr(canonicalStartPath.size(), canonicalStartPath.size() - canonicalPath.size())}; if (Utils::FileSystem::getParent(filePath).find(combinedPath) == 0) recursiveSymlink = true; } if (recursiveSymlink) { LOG(LogWarning) << "Skipped \"" << filePath << "\" as it's a recursive symlink"; continue; } } if (Utils::FileSystem::exists(filePath + "/noload.txt")) { #if defined(_WIN64) LOG(LogInfo) << "Skipped folder \"" << Utils::String::replace(filePath, "/", "\\") << "\" as a noload.txt file is present"; #else LOG(LogInfo) << "Skipped folder \"" << filePath << "\" as a noload.txt file is present"; #endif FileData* newFolder {new FileData(FOLDER, filePath, mEnvData, this)}; newFolder->setNoLoad(true); folder->addChild(newFolder); continue; } FileData* newFolder {new FileData(FOLDER, filePath, mEnvData, this)}; populateFolder(newFolder); if (mFlattenFolders) { for (auto& entry : newFolder->getChildrenByFilename()) folder->addChild(entry.second); } else { // Ignore folders that do not contain games. if (newFolder->getChildrenByFilename().size() == 0) delete newFolder; else folder->addChild(newFolder); } } } return true; } void SystemData::indexAllGameFilters(const FileData* folder) { const std::vector& children {folder->getChildren()}; for (std::vector::const_iterator it {children.cbegin()}; // Line break. it != children.cend(); ++it) { switch ((*it)->getType()) { case GAME: mFilterIndex->addToIndex(*it); break; case FOLDER: indexAllGameFilters(*it); break; default: break; } } } std::vector readList(const std::string& str, const std::string& delims = " \t\r\n,") { std::vector ret; size_t prevOff {str.find_first_not_of(delims, 0)}; size_t off {str.find_first_of(delims, prevOff)}; while (off != std::string::npos || prevOff != std::string::npos) { ret.emplace_back(str.substr(prevOff, off - prevOff)); prevOff = str.find_first_not_of(delims, off); off = str.find_first_of(delims, prevOff); } return ret; } bool SystemData::loadConfig() { deleteSystems(); if (sFindRules.get() == nullptr) sFindRules = std::make_unique(); LOG(LogInfo) << "Populating game systems..."; if (Settings::getInstance()->getBool("ParseGamelistOnly")) { LOG(LogInfo) << "Only parsing the gamelist.xml files, not scanning system directories"; } const std::vector& configPaths {getConfigPath()}; const std::string& rompath {FileData::getROMDirectory()}; bool onlyProcessCustomFile {false}; const bool splashScreen {Settings::getInstance()->getBool("SplashScreen")}; float systemCount {0.0f}; float parsedSystems {0.0f}; unsigned int gameCount {0}; // This is only done to get the total system count, for calculating the progress bar position. for (auto& configPath : configPaths) { pugi::xml_document doc; #if defined(_WIN64) const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(configPath).c_str())}; #else const pugi::xml_parse_result& res {doc.load_file(configPath.c_str())}; #endif if (!res) break; const pugi::xml_node& systemList {doc.child("systemList")}; if (!systemList) continue; for (pugi::xml_node system {systemList.child("system")}; system; system = system.next_sibling("system")) { ++systemCount; } if (doc.child("loadExclusive")) break; } for (auto& configPath : 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; #if defined(_WIN64) LOG(LogInfo) << "Parsing systems configuration file \"" << Utils::String::replace(configPath, "/", "\\") << "\"..."; #else LOG(LogInfo) << "Parsing systems configuration file \"" << configPath << "\"..."; #endif pugi::xml_document doc; #if defined(_WIN64) const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(configPath).c_str())}; #else const pugi::xml_parse_result& res {doc.load_file(configPath.c_str())}; #endif if (!res) { LOG(LogError) << "Couldn't parse es_systems.xml: " << res.description(); return true; } const pugi::xml_node& loadExclusive {doc.child("loadExclusive")}; if (loadExclusive) { if (configPath == configPaths.front() && configPaths.size() > 1) { LOG(LogInfo) << "Only loading custom file as the tag is present"; onlyProcessCustomFile = true; } 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"; } } // Actually read the file. const pugi::xml_node& systemList {doc.child("systemList")}; if (!systemList) { LOG(LogError) << "es_systems.xml is missing the tag"; return true; } unsigned int lastTime {0}; unsigned int accumulator {0}; SDL_Event event {}; for (pugi::xml_node system {systemList.child("system")}; system; system = system.next_sibling("system")) { // Poll events so that the OS doesn't think the application is hanging on startup, // this is required as the main application loop hasn't started yet. while (SDL_PollEvent(&event)) { InputManager::getInstance().parseEvent(event); if (event.type == SDL_QUIT) { sStartupExitSignal = true; return true; } #if defined(__ANDROID__) if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { ViewController::getInstance()->setWindowSizeChanged( static_cast(event.window.data1), static_cast(event.window.data2)); } #endif }; std::string name; std::string fullname; std::string sortName; std::string path; std::string themeFolder; name = Utils::String::replace(system.child("name").text().get(), "\n", ""); fullname = Utils::String::replace(system.child("fullname").text().get(), "\n", ""); sortName = system.child("systemsortname").text().get(); path = system.child("path").text().get(); if (splashScreen) { const unsigned int curTime {SDL_GetTicks()}; accumulator += curTime - lastTime; lastTime = curTime; ++parsedSystems; // This prevents Renderer::swapBuffers() from being called excessively which // could lead to significantly longer application startup times. if (accumulator > 40) { accumulator = 0; const float progress {glm::mix(0.0f, 0.5f, parsedSystems / systemCount)}; Window::getInstance()->renderSplashScreen(Window::SplashScreenState::SCANNING, progress); lastTime += SDL_GetTicks() - curTime; } } auto nameFindFunc = [&] { for (auto system : sSystemVector) { if (system->mName == name) { LOG(LogDebug) << "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, "\\", "/"); #endif path = Utils::String::replace(path, "//", "/"); // In case ~ is used, expand it to the home directory path. path = Utils::FileSystem::expandHomePath(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 #if defined(_WIN64) << "\" as the defined ROM directory \"" << Utils::String::replace(path, "/", "\\") #else << "\" as the defined ROM directory \"" << path #endif << "\" 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. const 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())}; // 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; } // Skip any duplicate entries (i.e. those with identical labels). bool duplicateLabel {false}; for (auto& command : commands) { if (command.second == entry.attribute("label").as_string()) { LOG(LogError) << "Duplicate command label \"" << entry.attribute("label").as_string() << "\" defined for system \"" << name << "\", ignoring entry"; duplicateLabel = true; break; } } if (!duplicateLabel) { commands.emplace_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"; } const std::vector& platformStrs {readList(platformList)}; std::vector platformIds; for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); ++it) { std::string str {*it}; const PlatformIds::PlatformId platformId {PlatformIds::getPlatformId(str)}; if (platformId == PlatformIds::PLATFORM_IGNORE) { // When platform is PLATFORM_IGNORE, do not allow other platforms. platformIds.clear(); platformIds.emplace_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.emplace_back(platformId); } // Theme folder. themeFolder = system.child("theme").text().as_string(name.c_str()); // 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 (sortName == "") sortName = fullname; // 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; } #endif // 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, sortName, 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 (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.emplace_back(newSys); gameCount += newSys->getRootFolder()->getGameCount().first; } } } if (splashScreen) { if (sSystemVector.size() > 0) Window::getInstance()->renderSplashScreen(Window::SplashScreenState::SCANNING, 0.5f); else Window::getInstance()->renderSplashScreen(Window::SplashScreenState::SCANNING, 1.0f); } loadSortingConfig(); LOG(LogInfo) << "Parsed configuration for " << systemCount << " system" << (systemCount == 1 ? ", loaded " : "s, loaded ") << sSystemVector.size() << " system" << (sSystemVector.size() == 1 ? "" : "s") << " (collections not included)"; LOG(LogInfo) << "Total game count: " << gameCount; // Sort systems by sortName, and always perform secondary sorting by the full name. std::sort(std::begin(sSystemVector), std::end(sSystemVector), [](SystemData* a, SystemData* b) { if (Utils::String::toUpper(a->getSortName()) < Utils::String::toUpper(b->getSortName())) return true; if (Utils::String::toUpper(a->getSortName()) > Utils::String::toUpper(b->getSortName())) return false; if (Utils::String::toUpper(a->getFullName()) < Utils::String::toUpper(b->getFullName())) return true; if (Utils::String::toUpper(a->getFullName()) > Utils::String::toUpper(b->getFullName())) return false; return false; }); // Don't load any collections if there are no systems available. if (sSystemVector.size() > 0) CollectionSystemsManager::getInstance()->loadCollectionSystems(); return false; } void SystemData::loadSortingConfig() { const std::string sortSetting {Settings::getInstance()->getString("SystemsSorting")}; const std::string customFilePath {Utils::FileSystem::getAppDataDirectory() + "/custom_systems/es_systems_sorting.xml"}; const bool customFileExists {Utils::FileSystem::exists(customFilePath)}; std::string path; bool bundledFile {false}; if (sortSetting == "hwtype_year") { path = ResourceManager::getInstance().getResourcePath( ":/sorting/hwtype_year/es_systems_sorting.xml", true); bundledFile = true; } else if (sortSetting == "manufacturer_hwtype_year") { path = ResourceManager::getInstance().getResourcePath( ":/sorting/manufacturer_hwtype_year/es_systems_sorting.xml", true); bundledFile = true; } else if (sortSetting == "manufacturer_year") { path = ResourceManager::getInstance().getResourcePath( ":/sorting/manufacturer_year/es_systems_sorting.xml", true); bundledFile = true; } else if (sortSetting == "year") { path = ResourceManager::getInstance().getResourcePath( ":/sorting/year/es_systems_sorting.xml", true); bundledFile = true; } if (bundledFile && customFileExists) { LOG(LogInfo) << "A custom systems sorting file was found but it will not get loaded as a " "bundled file has been selected"; } else if (!bundledFile && customFileExists) { path = customFilePath; } if (path.empty()) { LOG(LogDebug) << "No systems sorting file loaded"; return; } #if defined(_WIN64) LOG(LogInfo) << "Parsing systems sorting file \"" << Utils::String::replace(path, "/", "\\") << "\"..."; pugi::xml_document doc; const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(path).c_str())}; #else LOG(LogInfo) << "Parsing systems sorting file \"" << path << "\"..."; pugi::xml_document doc; const pugi::xml_parse_result& res {doc.load_file(path.c_str())}; #endif if (!res) { LOG(LogError) << "Couldn't parse es_systems_sorting.xml: " << res.description(); return; } const pugi::xml_node& systemList {doc.child("systemList")}; if (!systemList) { LOG(LogError) << "es_systems_sorting.xml is missing the tag"; return; } std::string systemName; std::string sortName; for (pugi::xml_node system {systemList.child("system")}; system; system = system.next_sibling("system")) { sortName = system.child("systemsortname").text().get(); if (sortName == "") continue; systemName = Utils::String::replace(system.child("name").text().get(), "\n", ""); auto systemEntry = std::find_if( sSystemVector.begin(), sSystemVector.end(), [systemName](SystemData* system) { return system->getName() == systemName; }); if (systemEntry != SystemData::sSystemVector.end()) (*systemEntry)->mSortName = sortName; } } std::string SystemData::getLaunchCommandFromLabel(const std::string& label) { auto commandIter = std::find_if( mEnvData->mLaunchCommands.cbegin(), mEnvData->mLaunchCommands.cend(), [label](std::pair command) { return (command.second == label); }); if (commandIter != mEnvData->mLaunchCommands.cend()) return (*commandIter).first; return ""; } void SystemData::deleteSystems() { for (unsigned int i {0}; i < sSystemVector.size(); ++i) delete sSystemVector.at(i); sSystemVector.clear(); } std::vector SystemData::getConfigPath() { std::vector paths; const std::string customSystemsDirectory {Utils::FileSystem::getAppDataDirectory() + "/custom_systems"}; if (!Utils::FileSystem::exists(customSystemsDirectory)) { LOG(LogInfo) << "Creating custom systems directory \"" << customSystemsDirectory << "\"..."; Utils::FileSystem::createDirectory(customSystemsDirectory); if (!Utils::FileSystem::exists(customSystemsDirectory)) { LOG(LogError) << "Couldn't create directory, permission problems?"; } } std::string path {customSystemsDirectory + "/es_systems.xml"}; if (Utils::FileSystem::exists(path)) { LOG(LogInfo) << "Found custom systems configuration file"; paths.emplace_back(path); } #if defined(__ANDROID__) path = ResourceManager::getInstance().getResourcePath(":/systems/android/es_systems.xml", true); #elif defined(__linux__) path = ResourceManager::getInstance().getResourcePath(":/systems/linux/es_systems.xml", true); #elif defined(_WIN64) path = ResourceManager::getInstance().getResourcePath(":/systems/windows/es_systems.xml", true); #elif defined(__APPLE__) path = ResourceManager::getInstance().getResourcePath(":/systems/macos/es_systems.xml", true); #elif defined(__HAIKU__) path = ResourceManager::getInstance().getResourcePath(":/systems/haiku/es_systems.xml", true); #else path = ResourceManager::getInstance().getResourcePath(":/systems/unix/es_systems.xml", true); #endif paths.emplace_back(path); return paths; } bool SystemData::createSystemDirectories() { std::vector configPaths {getConfigPath()}; const std::string& rompath {FileData::getROMDirectory()}; bool onlyProcessCustomFile {false}; LOG(LogInfo) << "Generating ROM directory structure..."; if (Utils::FileSystem::exists(rompath) && Utils::FileSystem::isRegularFile(rompath)) { LOG(LogError) << "Requested ROM directory \"" << rompath << "\" is actually a file, aborting"; return true; } if (!Utils::FileSystem::exists(rompath)) { #if defined(_WIN64) if (rompath.size() == 3 && rompath[1] == ':' && rompath[2] == '\\') { if (Utils::FileSystem::driveExists(rompath)) { LOG(LogInfo) << "ROM directory set to root of device " << rompath; } else { LOG(LogError) << "Device " << rompath << " does not exist"; return true; } } else { LOG(LogInfo) << "Creating base ROM directory \"" << rompath << "\"..."; if (!Utils::FileSystem::createDirectory(rompath)) { LOG(LogError) << "Couldn't create directory, permission problems or disk full?"; return true; } } #else LOG(LogInfo) << "Creating base ROM directory \"" << rompath << "\"..."; if (!Utils::FileSystem::createDirectory(rompath)) { LOG(LogError) << "Couldn't create directory, permission problems or disk full?"; return true; } #endif } else { LOG(LogInfo) << "Base ROM directory \"" << rompath << "\" already exists"; } 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) const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(configPaths.front()).c_str())}; #else const pugi::xml_parse_result& res {doc.load_file(configPaths.front().c_str())}; #endif if (res) { const pugi::xml_node& loadExclusive {doc.child("loadExclusive")}; if (loadExclusive) onlyProcessCustomFile = true; } } // 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; #if defined(_WIN64) LOG(LogInfo) << "Parsing systems configuration file \"" << Utils::String::replace(configPath, "/", "\\") << "\"..."; #else LOG(LogInfo) << "Parsing systems configuration file \"" << configPath << "\"..."; #endif pugi::xml_document doc; #if defined(_WIN64) const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(configPath).c_str())}; #else const pugi::xml_parse_result& res {doc.load_file(configPath.c_str())}; #endif if (!res) { LOG(LogError) << "Couldn't parse es_systems.xml"; LOG(LogError) << res.description(); return true; } // Actually read the file. const 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 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.emplace_back(entry.text().get()); } platform = Utils::String::toLower(system.child("platform").text().get()); const bool multiplePlatforms { std::find_if(platform.cbegin(), platform.cend(), [](char character) { return (std::isspace(character) || character == ','); }) != platform.cend()}; 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 (commands.size() == 1 && Utils::String::toLower(commands.front()).find("placeholder") != std::string::npos) { if (Settings::getInstance()->getBool("CreatePlaceholderSystemDirectories")) { LOG(LogInfo) << "System \"" << name << "\" is a placeholder but creating directory anyway as " "CreatePlaceholderSystemDirectories is set to true"; } else { LOG(LogInfo) << "System \"" << name << "\" is a placeholder, skipping entry"; continue; } } 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" << (multiplePlatforms ? "s" : "") << " (for scraping):" << std::endl; if (platform.empty()) systemInfoFile << "None defined" << std::endl << std::endl; else 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.emplace_back( std::make_pair(systemDir + " (custom system)", fullname)); else systemsVector.emplace_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 << "\""; } } } // Also generate a systems.txt file directly in the ROM directory root that contains the // mappings between the system directory names and the full system names. This makes it // easier for the users to identify the correct directories for their games. if (!systemsVector.empty()) { const std::string& systemsFileName {"/systems.txt"}; bool systemsFileSuccess {true}; if (Utils::FileSystem::exists(rompath + systemsFileName)) { if (!Utils::FileSystem::removeFile(rompath + systemsFileName)) systemsFileSuccess = false; } if (systemsFileSuccess) { std::ofstream systemsFile; #if defined(_WIN64) systemsFile.open(Utils::String::stringToWideString(rompath + systemsFileName).c_str()); #else systemsFile.open(rompath + systemsFileName); #endif if (systemsFile.fail()) { systemsFileSuccess = false; } else { std::sort(systemsVector.begin(), systemsVector.end()); for (auto systemEntry : systemsVector) { systemsFile << systemEntry.first.append(": ").append(systemEntry.second) << std::endl; } systemsFile.close(); } } if (!systemsFileSuccess) { LOG(LogWarning) << "System directories successfully created but couldn't create " "the systems.txt file in the ROM directory root"; return false; } } LOG(LogInfo) << "System directories successfully created"; return false; } SystemData* SystemData::getSystemByName(const std::string& systemName) { for (auto it : sSystemVector) { if ((*it).getName() == systemName) return it; } return nullptr; } SystemData* SystemData::getNext() const { auto it = std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); ++it; if (it == sSystemVector.cend()) it = sSystemVector.cbegin(); return *it; } SystemData* SystemData::getPrev() const { auto it = std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); ++it; if (it == sSystemVector.crend()) it = sSystemVector.crbegin(); return *it; } std::string SystemData::getGamelistPath(bool forWrite) const { std::string filePath {mRootFolder->getPath() + "/gamelist.xml"}; const std::string gamelistPath {Utils::FileSystem::getAppDataDirectory() + "/gamelists/" + mName}; if (Utils::FileSystem::exists(filePath)) { if (Settings::getInstance()->getBool("LegacyGamelistFileLocation")) { return filePath; } else { #if defined(_WIN64) LOG(LogWarning) << "Found a gamelist.xml file in \"" << Utils::String::replace(mRootFolder->getPath(), "/", "\\") << "\\\" which will not get loaded, move it to \"" << gamelistPath << "\\\" or otherwise delete it"; #else LOG(LogWarning) << "Found a gamelist.xml file in \"" << mRootFolder->getPath() << "/\" which will not get loaded, move it to \"" << gamelistPath << "/\" or otherwise delete it"; #endif } } filePath = gamelistPath + "/gamelist.xml"; // Make sure the directory exists if we're going to write to it, or crashes will happen. if (forWrite) Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); if (forWrite || Utils::FileSystem::exists(filePath)) return filePath; return ""; } std::string SystemData::getThemePath() const { // Check for the presence of the system theme file ./systemname/theme.xml and if this does not // exist, then try the default file for the theme, i.e. ./theme.xml std::string systemThemeFile {ThemeData::getSystemThemeFile(mThemeFolder)}; if (Utils::FileSystem::exists(systemThemeFile)) return systemThemeFile; systemThemeFile = Utils::FileSystem::getParent(Utils::FileSystem::getParent(systemThemeFile)); if (systemThemeFile != "") { systemThemeFile.append("/theme.xml"); if (Utils::FileSystem::exists(systemThemeFile)) return systemThemeFile; } return ""; } SystemData* SystemData::getRandomSystem(const SystemData* currentSystem) { int total {0}; for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); ++it) { if ((*it)->isGameSystem()) ++total; } if (total < 2) return nullptr; SystemData* randomSystem {nullptr}; do { // Get a random number in range. std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; std::uniform_int_distribution uniform_dist {0, total - 1}; int target {uniform_dist(engine)}; for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); ++it) { if ((*it)->isGameSystem()) { if (target > 0) { --target; } else { randomSystem = (*it); break; } } } } while (randomSystem == currentSystem); return randomSystem; } FileData* SystemData::getRandomGame(const FileData* currentGame, bool gameSelectorMode) { std::vector gameList; bool onlyFolders {false}; bool hasFolders {false}; // If we're in the custom collection group list, then get the list of collections, // otherwise get a list of all the folder and file entries in the view. if (currentGame && currentGame->getType() == FOLDER && currentGame->getSystem()->isGroupedCustomCollection()) { gameList = mRootFolder->getParent()->getChildrenListToDisplay(); } else { if (gameSelectorMode) { gameList = mRootFolder->getFilesRecursive(GAME, false, false); if (Settings::getInstance()->getString("UIMode") == "kid") { // Doing some extra work here instead of in FileData is OK as it's only needed // for the rare combination of a gameselector being present while in kid mode. for (auto it = gameList.begin(); it != gameList.end();) { if (!(*it)->getKidgame()) it = gameList.erase(it); else ++it; } } } else { gameList = ViewController::getInstance() ->getGamelistView(mRootFolder->getSystem()) .get() ->getCursor() ->getParent() ->getChildrenListToDisplay(); } } if (gameList.size() > 0 && gameList.front()->getParent()->getOnlyFoldersFlag()) onlyFolders = true; if (gameList.size() > 0 && gameList.front()->getParent()->getHasFoldersFlag()) hasFolders = true; // If this is a mixed view of folders and files, then remove all the folder entries // as we want to exclude them from the random selection. if (!onlyFolders && hasFolders) { unsigned int i {0}; do { if (gameList[i]->getType() == FOLDER) gameList.erase(gameList.begin() + i); else ++i; } while (i < gameList.size()); } if (!currentGame && gameList.size() == 1) return gameList.front(); // If there is only one folder and one file in the list, then return the file. if (!onlyFolders && hasFolders && gameList.size() == 1) return gameList.front(); if (currentGame && currentGame->getType() == PLACEHOLDER) return nullptr; int total {static_cast(gameList.size())}; int target {0}; if (total < 2) return nullptr; do { // Get a random number in range. std::random_device randDev; // Mersenne Twister pseudorandom number generator. std::mt19937 engine {randDev()}; std::uniform_int_distribution uniform_dist {0, total - 1}; target = uniform_dist(engine); } while (currentGame && gameList.at(target) == currentGame); return gameList.at(target); } void SystemData::sortSystem(bool reloadGamelist, bool jumpToFirstRow) { if (getName() == "recent") return; bool favoritesSorting {false}; if (this->isCustomCollection() || (this->isCollection() && this->getFullName() == "collections")) { favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); } else { favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); } FileData* rootFolder {getRootFolder()}; // Assign the sort type to all grouped custom collections. if (mIsCollectionSystem && mFullName == "collections") { for (auto it = rootFolder->getChildren().begin(); // Line break. it != rootFolder->getChildren().end(); ++it) { setupSystemSortType((*it)->getSystem()->getRootFolder()); } } setupSystemSortType(rootFolder); rootFolder->sort(rootFolder->getSortTypeFromString(rootFolder->getSortTypeString()), favoritesSorting); if (reloadGamelist) ViewController::getInstance()->reloadGamelistView(this, false); if (jumpToFirstRow) { GamelistView* gameList {ViewController::getInstance()->getGamelistView(this).get()}; gameList->setCursor(gameList->getFirstEntry()); } } std::pair SystemData::getDisplayedGameCount() const { // Return all games for the system which are marked as 'countasgame'. As this flag is set // by default, normally most games will be included in the number returned from here. // The actual game counting takes place in FileData during sorting. return mRootFolder->getGameCount(); } void SystemData::loadTheme(ThemeTriggers::TriggerType trigger) { mTheme = std::make_shared(); const std::string& path {getThemePath()}; if (!Utils::FileSystem::exists(path)) { // No theme available for this platform. if (!mIsCustomCollectionSystem) { LOG(LogWarning) << "There is no \"" << mThemeFolder << "\" configuration available for the selected theme \"" << Settings::getInstance()->getString("Theme") << "\", system will be unthemed"; } return; } try { // Build a map with system variables for the theme to use. Assign a backspace character // to the variables that are not applicable. This will be used in ThemeData to make sure // unpopulated system variables do not lead to theme loading errors. std::map sysData; std::string name {getName()}; std::string fullName {getFullName()}; #if defined(GETTEXT_DUMMY_ENTRIES) _p("theme", "all"); _p("theme", "all games"); _p("theme", "recent"); _p("theme", "last played"); _p("theme", "favorites"); _p("theme", "collections"); #endif // Always translate fullName for the automatic collections. if (isCollection() && !isCustomCollection()) { name = _p("theme", name.c_str()); fullName = _p("theme", fullName.c_str()); } sysData.insert(std::pair("system.name", name)); sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", fullName)); if (isCollection() && isCustomCollection()) { sysData.insert( std::pair("system.name.customCollections", name)); sysData.insert( std::pair("system.fullName.customCollections", fullName)); sysData.insert(std::pair("system.theme.customCollections", getThemeFolder())); sysData.insert( std::pair("system.name.autoCollections", "\b")); sysData.insert( std::pair("system.fullName.autoCollections", "\b")); sysData.insert( std::pair("system.theme.autoCollections", "\b")); sysData.insert(std::pair("system.name.noCollections", "\b")); sysData.insert( std::pair("system.fullName.noCollections", "\b")); sysData.insert(std::pair("system.theme.noCollections", "\b")); } else if (isCollection()) { sysData.insert( std::pair("system.name.autoCollections", name)); sysData.insert( std::pair("system.fullName.autoCollections", fullName)); sysData.insert(std::pair("system.theme.autoCollections", getThemeFolder())); sysData.insert( std::pair("system.name.customCollections", "\b")); sysData.insert( std::pair("system.fullName.customCollections", "\b")); sysData.insert( std::pair("system.theme.customCollections", "\b")); sysData.insert(std::pair("system.name.noCollections", "\b")); sysData.insert( std::pair("system.fullName.noCollections", "\b")); sysData.insert(std::pair("system.theme.noCollections", "\b")); } else { sysData.insert(std::pair("system.name.noCollections", name)); sysData.insert( std::pair("system.fullName.noCollections", fullName)); sysData.insert(std::pair("system.theme.noCollections", getThemeFolder())); sysData.insert( std::pair("system.name.autoCollections", "\b")); sysData.insert( std::pair("system.fullName.autoCollections", "\b")); sysData.insert( std::pair("system.theme.autoCollections", "\b")); sysData.insert( std::pair("system.name.customCollections", "\b")); sysData.insert( std::pair("system.fullName.customCollections", "\b")); sysData.insert( std::pair("system.theme.customCollections", "\b")); } mTheme->loadFile(sysData, path, trigger, isCustomCollection()); } catch (ThemeException& e) { LOG(LogError) << e.what() << " (system \"" << mName << "\", theme \"" << mThemeFolder << "\")"; mTheme = std::make_shared(); // Reset to empty. } } void SystemData::writeMetaData() { if (Settings::getInstance()->getBool("IgnoreGamelist") || mIsCollectionSystem) return; // Save changed game data back to xml. GamelistFileParser::updateGamelist(this); } void SystemData::onMetaDataSavePoint() { if (Settings::getInstance()->getString("SaveGamelistsMode") != "always") return; writeMetaData(); } void SystemData::setupSystemSortType(FileData* rootFolder) { // If DefaultSortOrder is set to something, check that it is actually a valid value. if (Settings::getInstance()->getString("DefaultSortOrder") != "") { for (unsigned int i {0}; i < FileSorts::SortTypes.size(); ++i) { if (FileSorts::SortTypes.at(i).description == Settings::getInstance()->getString("DefaultSortOrder")) { rootFolder->setSortTypeString( Settings::getInstance()->getString("DefaultSortOrder")); break; } } } // If no valid sort type was defined in the configuration file, set to default sorting. if (rootFolder->getSortTypeString() == "") rootFolder->setSortTypeString( Settings::getInstance()->getDefaultString("DefaultSortOrder")); }