// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // Gamelist.cpp // // Parses and updates the gamelist.xml files. // #include "Gamelist.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include "FileData.h" #include "Log.h" #include "Settings.h" #include "SystemData.h" #include FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type) { // First, verify that path is within the system's root folder. FileData* root = system->getRootFolder(); bool contains = false; std::string relative = Utils::FileSystem::removeCommonPath(path, root->getPath(), contains); if (!contains) { LOG(LogError) << "Path \"" << path << "\" is outside system path \"" << system->getStartPath() << "\""; return nullptr; } Utils::FileSystem::stringList pathList = Utils::FileSystem::getPathList(relative); auto path_it = pathList.begin(); FileData* treeNode = root; bool found = false; while (path_it != pathList.end()) { const std::unordered_map& children = treeNode->getChildrenByFilename(); std::string key = *path_it; found = children.find(key) != children.cend(); if (found) { treeNode = children.at(key); } // This is the end if (path_it == --pathList.end()) { if (found) return treeNode; if (type == FOLDER) { LOG(LogWarning) << "A folder defined in the gamelist file does not exist:"; return nullptr; } FileData* file = new FileData(type, path, system->getSystemEnvData(), system); // Skipping arcade assets from gamelist. if (!file->isArcadeAsset()) treeNode->addChild(file); return file; } if (!found) { // Don't create folders unless they're including any games. // If the type is FOLDER it's going to be empty, so don't bother. if (type == FOLDER) { LOG(LogWarning) << "A folder defined in the gamelist file does not exist:"; return nullptr; } // Create missing folder. FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); treeNode->addChild(folder); treeNode = folder; } path_it++; } return nullptr; } void parseGamelist(SystemData* system) { bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); std::string xmlpath = system->getGamelistPath(false); if (!Utils::FileSystem::exists(xmlpath)) { LOG(LogInfo) << "Loading system \"" << system->getName() << "\" which does not have a gamelist.xml file..."; return; } LOG(LogInfo) << "Parsing gamelist file \"" << xmlpath << "\"..."; pugi::xml_document doc; #if defined(_WIN64) pugi::xml_parse_result result = doc.load_file(Utils::String::stringToWideString(xmlpath).c_str()); #else pugi::xml_parse_result result = doc.load_file(xmlpath.c_str()); #endif if (!result) { LOG(LogError) << "Error parsing gamelist file \"" << xmlpath << "\": " << result.description(); return; } pugi::xml_node root = doc.child("gameList"); if (!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlpath << "\""; return; } std::string relativeTo = system->getStartPath(); bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); std::vector tagList = { "game", "folder" }; FileType typeList[2] = { GAME, FOLDER }; for (int i = 0; i < 2; i++) { std::string tag = tagList[i]; FileType type = typeList[i]; for (pugi::xml_node fileNode = root.child(tag.c_str()); fileNode; fileNode = fileNode.next_sibling(tag.c_str())) { const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); if (!trustGamelist && !Utils::FileSystem::exists(path)) { LOG(LogWarning) << (type == GAME ? "File \"" : "Folder \"") << path << "\" does not exist, ignoring entry"; continue; } // Skip hidden files, check both the file itself and the directory in which // it is located. if (!showHiddenFiles && (Utils::FileSystem::isHidden(path) || Utils::FileSystem::isHidden(Utils::FileSystem::getParent(path)))) { LOG(LogDebug) << "Gamelist::parseGamelist(): Skipping hidden file \"" << path << "\""; continue; } FileData* file = findOrCreateFile(system, path, type); if (!file) { LOG(LogError) << "Could not find or create \"" << path << "\", skipping entry"; continue; } else if (!file->isArcadeAsset()) { std::string defaultName = file->metadata.get("name"); file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo); // Make sure a name gets set if one doesn't exist. if (file->metadata.get("name").empty()) file->metadata.set("name", defaultName); file->metadata.resetChangedFlag(); } // If the game is flagged as hidden and the option has not been set to show hidden // games, then delete the entry. This leaves no trace of the entry at all in ES // but that is fine as the option to show hidden files is defined as requiring an // application restart. if (!Settings::getInstance()->getBool("ShowHiddenGames")) { if (file->getHidden()) { LOG(LogDebug) << "Gamelist::parseGamelist(): Skipping hidden " << (type == GAME ? "file" : "folder") << " entry \"" << file->getName() << "\"" << " (\"" << file->getPath() << "\")"; delete file; } // Also delete any folders which are empty, i.e. all their entries are hidden. else if (file->getType() == FOLDER && file->getChildren().size() == 0) { delete file; } } } } } void addFileDataNode(pugi::xml_node& parent, const FileData* file, const std::string& tag, SystemData* system) { // Create game and add to parent node. pugi::xml_node newNode = parent.append_child(tag.c_str()); // Write metadata. file->metadata.appendToXML(newNode, true, system->getStartPath()); // First element is "name", there's only one element and the name is the default. if (newNode.children().begin() == newNode.child("name") && ++newNode.children().begin() == newNode.children().end() && newNode.child("name").text().get() == file->getDisplayName()) { // If the only info is the default name, don't bother // with this node, delete it and ultimately do nothing. parent.remove_child(newNode); } else { // There's something useful in there so we'll keep the node, add the path. // Try and make the path relative if we can so things still // work if we change the ROM folder location in the future. newNode.prepend_child("path").text().set(Utils::FileSystem::createRelativePath(file-> getPath(), system->getStartPath(), false).c_str()); } } void updateGamelist(SystemData* system) { // We do this by reading the XML again, adding changes and then writing them back, // because there might be information missing in our systemdata which we would otherwise // miss in the new XML file. We have the complete information for every game though, so // we can simply remove a game we already have in the system from the XML, and then add // it back from its GameData information... if (Settings::getInstance()->getBool("IgnoreGamelist")) return; pugi::xml_document doc; pugi::xml_node root; std::string xmlReadPath = system->getGamelistPath(false); if (Utils::FileSystem::exists(xmlReadPath)) { // Parse an existing file first. #if defined(_WIN64) pugi::xml_parse_result result = doc.load_file(Utils::String::stringToWideString(xmlReadPath).c_str()); #else pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); #endif if (!result) { LOG(LogError) << "Error parsing gamelist file \"" << xmlReadPath << "\": " << result.description(); return; } root = doc.child("gameList"); if (!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlReadPath << "\""; return; } } else { // Set up an empty gamelist to append to. root = doc.append_child("gameList"); } // Now we have all the information from the XML file, so iterate // through all our games and add the information from there. FileData* rootFolder = system->getRootFolder(); if (rootFolder != nullptr) { int numUpdated = 0; // Get only files, no folders. std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); // Iterate through all files, checking if they're already in the XML file. for (std::vector::const_iterator fit = files.cbegin(); fit != files.cend(); fit++) { const std::string tag = ((*fit)->getType() == GAME) ? "game" : "folder"; // Do not touch if it wasn't changed and is not flagged for deletion. if (!(*fit)->metadata.wasChanged() && !(*fit)->getDeletionFlag()) continue; // Check if the file already exists in the XML file. // If it does, remove the entry before adding it back. for (pugi::xml_node fileNode = root.child(tag.c_str()); fileNode; fileNode = fileNode.next_sibling(tag.c_str())) { pugi::xml_node pathNode = fileNode.child("path"); if (!pathNode) { LOG(LogError) << "<" << tag << "> node contains no child"; continue; } std::string nodePath =Utils::FileSystem::getCanonicalPath( Utils::FileSystem::resolveRelativePath(pathNode.text().get(), system->getStartPath(), true)); std::string gamePath = Utils::FileSystem::getCanonicalPath((*fit)->getPath()); if (nodePath == gamePath) { // Found it root.remove_child(fileNode); if ((*fit)->getDeletionFlag()) numUpdated++; break; } } // Add the game to the file, unless it's flagged for deletion. if (!(*fit)->getDeletionFlag()) { addFileDataNode(root, *fit, tag, system); (*fit)->metadata.resetChangedFlag(); numUpdated++; } } // Now write the file. if (numUpdated > 0) { // Make sure the folders leading up to this path exist (or the write will fail). std::string xmlWritePath(system->getGamelistPath(true)); Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); LOG(LogDebug) << "Gamelist::updateGamelist(): Added/updated " << numUpdated << (numUpdated == 1 ? " entity in \"" : " entities in \"") << xmlReadPath << "\""; #if defined(_WIN64) if (!doc.save_file(Utils::String::stringToWideString(xmlWritePath).c_str())) { #else if (!doc.save_file(xmlWritePath.c_str())) { #endif LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")"; } } } else { LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\""; } }