From 97e08922a094605fba8ba2f2ce361d8ab7994ab9 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 15 Jan 2022 14:12:56 +0100 Subject: [PATCH] Renamed GamelistFileParse to GamelistFileParser and moved it to its own namespace. --- es-app/CMakeLists.txt | 4 +- es-app/src/GamelistFileParse.cpp | 425 ------------------- es-app/src/GamelistFileParse.h | 20 - es-app/src/GamelistFileParser.cpp | 433 ++++++++++++++++++++ es-app/src/GamelistFileParser.h | 24 ++ es-app/src/SystemData.cpp | 6 +- es-app/src/guis/GuiAlternativeEmulators.cpp | 4 +- es-app/src/guis/GuiMetaDataEd.cpp | 4 +- es-app/src/guis/GuiScraperMulti.cpp | 4 +- 9 files changed, 468 insertions(+), 456 deletions(-) delete mode 100644 es-app/src/GamelistFileParse.cpp delete mode 100644 es-app/src/GamelistFileParse.h create mode 100644 es-app/src/GamelistFileParser.cpp create mode 100644 es-app/src/GamelistFileParser.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 15a593b0c..707d86f26 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -15,7 +15,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h ${CMAKE_CURRENT_SOURCE_DIR}/src/FileSorts.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/GamelistFileParse.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/GamelistFileParser.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MediaViewer.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.h @@ -64,7 +64,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileSorts.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/GamelistFileParse.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/GamelistFileParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MediaViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.cpp diff --git a/es-app/src/GamelistFileParse.cpp b/es-app/src/GamelistFileParse.cpp deleted file mode 100644 index 1b3341073..000000000 --- a/es-app/src/GamelistFileParse.cpp +++ /dev/null @@ -1,425 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// EmulationStation Desktop Edition -// GamelistFileParse.cpp -// -// Parses and updates the gamelist.xml files. -// - -#include "GamelistFileParse.h" - -#include "FileData.h" -#include "Log.h" -#include "Settings.h" -#include "SystemData.h" -#include "utils/FileSystemUtil.h" -#include "utils/StringUtil.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; - } - - // Handle the special situation where a file exists and has an entry in the - // gamelist.xml file but the file extension is not configured in es_systems.xml. - const std::vector extensions = - system->getSystemEnvData()->mSearchExtensions; - - if (std::find(extensions.cbegin(), extensions.cend(), - Utils::FileSystem::getExtension(path)) == extensions.cend()) { - LOG(LogWarning) << "File \"" << path - << "\" is present in gamelist.xml but the extension is not " - "configured in es_systems.xml"; - 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(LogDebug) << "Gamelist::parseGamelist(): System \"" << system->getName() - << "\" 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) << "Couldn't find node in gamelist \"" << xmlpath << "\""; - return; - } - - pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); - if (alternativeEmulator) { - std::string label = alternativeEmulator.child("label").text().get(); - if (label != "") { - bool validLabel = false; - for (auto command : system->getSystemEnvData()->mLaunchCommands) { - if (command.second == label) - validLabel = true; - } - if (validLabel) { - system->setAlternativeEmulator(label); - LOG(LogDebug) << "Gamelist::parseGamelist(): System \"" << system->getName() - << "\" has a valid alternativeEmulator entry: \"" << label << "\""; - } - else { - system->setAlternativeEmulator("" + label); - LOG(LogWarning) << "System \"" << system->getName() - << "\" has an invalid alternativeEmulator entry that does " - "not match any command tag in es_systems.xml: \"" - << label << "\""; - } - } - } - - 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) << "Couldn't 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(); - } - else { - // Skip arcade asset entries as these will not be used in any way inside - // the application. - LOG(LogDebug) << "Gamelist::parseGamelist(): Skipping arcade asset \"" - << file->getName() << "\""; - delete file; - continue; - } - // 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, bool updateAlternativeEmulator) -{ - // 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); - bool hasAlternativeEmulatorTag = 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) << "Couldn't find node in gamelist \"" << xmlReadPath << "\""; - return; - } - if (updateAlternativeEmulator) { - pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); - - if (alternativeEmulator) - hasAlternativeEmulatorTag = true; - - if (system->getAlternativeEmulator() != "") { - if (!alternativeEmulator) { - doc.prepend_child("alternativeEmulator"); - alternativeEmulator = doc.child("alternativeEmulator"); - } - - pugi::xml_node label = alternativeEmulator.child("label"); - - if (label && system->getAlternativeEmulator() != - alternativeEmulator.child("label").text().get()) { - alternativeEmulator.remove_child(label); - alternativeEmulator.prepend_child("label").text().set( - system->getAlternativeEmulator().c_str()); - } - else if (!label) { - alternativeEmulator.prepend_child("label").text().set( - system->getAlternativeEmulator().c_str()); - } - } - else if (alternativeEmulator) { - doc.remove_child("alternativeEmulator"); - } - } - } - else { - if (updateAlternativeEmulator && system->getAlternativeEmulator() != "") { - pugi::xml_node alternativeEmulator = doc.prepend_child("alternativeEmulator"); - alternativeEmulator.prepend_child("label").text().set( - system->getAlternativeEmulator().c_str()); - } - // 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(); // Line break. - 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 || updateAlternativeEmulator) { - // 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)); - - if (updateAlternativeEmulator) { - if (hasAlternativeEmulatorTag && system->getAlternativeEmulator() == "") { - LOG(LogDebug) << "Gamelist::updateGamelist(): Removed the " - "alternativeEmulator tag for system \"" - << system->getName() << "\" as the default emulator \"" - << system->getSystemEnvData()->mLaunchCommands.front().second - << "\" was selected"; - } - else if (system->getAlternativeEmulator() != "") { - LOG(LogDebug) << "Gamelist::updateGamelist(): " - "Added/updated the alternativeEmulator tag for system \"" - << system->getName() << "\" to \"" - << system->getAlternativeEmulator() << "\""; - } - } - if (numUpdated > 0) { - LOG(LogDebug) << "Gamelist::updateGamelist(): Added/updated " << numUpdated - << (numUpdated == 1 ? " entity in \"" : " entities in \"") - << xmlWritePath << "\""; - } -#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() << "\""; - } -} diff --git a/es-app/src/GamelistFileParse.h b/es-app/src/GamelistFileParse.h deleted file mode 100644 index fb3da8057..000000000 --- a/es-app/src/GamelistFileParse.h +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// EmulationStation Desktop Edition -// GamelistFileParse.h -// -// Parses and updates the gamelist.xml files. -// - -#ifndef ES_APP_GAMELIST_FILE_PARSE_H -#define ES_APP_GAMELIST_FILE_PARSE_H - -class SystemData; - -// Loads gamelist.xml data into a SystemData. -void parseGamelist(SystemData* system); - -// Writes currently loaded metadata for a SystemData to gamelist.xml. -void updateGamelist(SystemData* system, bool updateAlternativeEmulator = false); - -#endif // ES_APP_GAMELIST_FILE_PARSE_H diff --git a/es-app/src/GamelistFileParser.cpp b/es-app/src/GamelistFileParser.cpp new file mode 100644 index 000000000..5e558d114 --- /dev/null +++ b/es-app/src/GamelistFileParser.cpp @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GamelistFileParser.cpp +// +// Parses and updates the gamelist.xml files. +// + +#include "GamelistFileParser.h" + +#include "FileData.h" +#include "Log.h" +#include "Settings.h" +#include "SystemData.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +#include + +namespace GamelistFileParser +{ + 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; + } + + // Handle the special situation where a file exists and has an entry in the + // gamelist.xml file but the file extension is not configured in es_systems.xml. + const std::vector extensions = + system->getSystemEnvData()->mSearchExtensions; + + if (std::find(extensions.cbegin(), extensions.cend(), + Utils::FileSystem::getExtension(path)) == extensions.cend()) { + LOG(LogWarning) << "File \"" << path + << "\" is present in gamelist.xml but the extension is not " + "configured in es_systems.xml"; + 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(LogDebug) << "Gamelist::parseGamelist(): System \"" << system->getName() + << "\" 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) << "Couldn't find node in gamelist \"" << xmlpath << "\""; + return; + } + + pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); + if (alternativeEmulator) { + std::string label = alternativeEmulator.child("label").text().get(); + if (label != "") { + bool validLabel = false; + for (auto command : system->getSystemEnvData()->mLaunchCommands) { + if (command.second == label) + validLabel = true; + } + if (validLabel) { + system->setAlternativeEmulator(label); + LOG(LogDebug) << "Gamelist::parseGamelist(): System \"" << system->getName() + << "\" has a valid alternativeEmulator entry: \"" << label + << "\""; + } + else { + system->setAlternativeEmulator("" + label); + LOG(LogWarning) << "System \"" << system->getName() + << "\" has an invalid alternativeEmulator entry that does " + "not match any command tag in es_systems.xml: \"" + << label << "\""; + } + } + } + + 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) << "Couldn't 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(); + } + else { + // Skip arcade asset entries as these will not be used in any way inside + // the application. + LOG(LogDebug) << "Gamelist::parseGamelist(): Skipping arcade asset \"" + << file->getName() << "\""; + delete file; + continue; + } + // 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, bool updateAlternativeEmulator) + { + // 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); + bool hasAlternativeEmulatorTag = 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) << "Couldn't find node in gamelist \"" << xmlReadPath + << "\""; + return; + } + if (updateAlternativeEmulator) { + pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); + + if (alternativeEmulator) + hasAlternativeEmulatorTag = true; + + if (system->getAlternativeEmulator() != "") { + if (!alternativeEmulator) { + doc.prepend_child("alternativeEmulator"); + alternativeEmulator = doc.child("alternativeEmulator"); + } + + pugi::xml_node label = alternativeEmulator.child("label"); + + if (label && system->getAlternativeEmulator() != + alternativeEmulator.child("label").text().get()) { + alternativeEmulator.remove_child(label); + alternativeEmulator.prepend_child("label").text().set( + system->getAlternativeEmulator().c_str()); + } + else if (!label) { + alternativeEmulator.prepend_child("label").text().set( + system->getAlternativeEmulator().c_str()); + } + } + else if (alternativeEmulator) { + doc.remove_child("alternativeEmulator"); + } + } + } + else { + if (updateAlternativeEmulator && system->getAlternativeEmulator() != "") { + pugi::xml_node alternativeEmulator = doc.prepend_child("alternativeEmulator"); + alternativeEmulator.prepend_child("label").text().set( + system->getAlternativeEmulator().c_str()); + } + // 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(); // Line break. + 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 || updateAlternativeEmulator) { + // 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)); + + if (updateAlternativeEmulator) { + if (hasAlternativeEmulatorTag && system->getAlternativeEmulator() == "") { + LOG(LogDebug) << "Gamelist::updateGamelist(): Removed the " + "alternativeEmulator tag for system \"" + << system->getName() << "\" as the default emulator \"" + << system->getSystemEnvData()->mLaunchCommands.front().second + << "\" was selected"; + } + else if (system->getAlternativeEmulator() != "") { + LOG(LogDebug) << "Gamelist::updateGamelist(): " + "Added/updated the alternativeEmulator tag for system \"" + << system->getName() << "\" to \"" + << system->getAlternativeEmulator() << "\""; + } + } + if (numUpdated > 0) { + LOG(LogDebug) << "Gamelist::updateGamelist(): Added/updated " << numUpdated + << (numUpdated == 1 ? " entity in \"" : " entities in \"") + << xmlWritePath << "\""; + } +#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() << "\""; + } + } + +} // namespace GamelistFileParser diff --git a/es-app/src/GamelistFileParser.h b/es-app/src/GamelistFileParser.h new file mode 100644 index 000000000..465c3e462 --- /dev/null +++ b/es-app/src/GamelistFileParser.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GamelistFileParser.h +// +// Parses and updates the gamelist.xml files. +// + +#ifndef ES_APP_GAMELIST_FILE_PARSER_H +#define ES_APP_GAMELIST_FILE_PARSER_H + +class SystemData; + +namespace GamelistFileParser +{ + // Loads gamelist.xml data into a SystemData. + void parseGamelist(SystemData* system); + + // Writes currently loaded metadata for a SystemData to gamelist.xml. + void updateGamelist(SystemData* system, bool updateAlternativeEmulator = false); + +} // namespace GamelistFileParser + +#endif // ES_APP_GAMELIST_FILE_PARSER_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 10b246d69..bee2823a6 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -14,7 +14,7 @@ #include "CollectionSystemsManager.h" #include "FileFilterIndex.h" #include "FileSorts.h" -#include "GamelistFileParse.h" +#include "GamelistFileParser.h" #include "Log.h" #include "Settings.h" #include "ThemeData.h" @@ -212,7 +212,7 @@ SystemData::SystemData(const std::string& name, } if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); + GamelistFileParser::parseGamelist(this); setupSystemSortType(mRootFolder); @@ -1255,7 +1255,7 @@ void SystemData::writeMetaData() return; // Save changed game data back to xml. - updateGamelist(this); + GamelistFileParser::updateGamelist(this); } void SystemData::onMetaDataSavePoint() diff --git a/es-app/src/guis/GuiAlternativeEmulators.cpp b/es-app/src/guis/GuiAlternativeEmulators.cpp index 94d7aaee6..64ee12c78 100644 --- a/es-app/src/guis/GuiAlternativeEmulators.cpp +++ b/es-app/src/guis/GuiAlternativeEmulators.cpp @@ -9,7 +9,7 @@ #include "guis/GuiAlternativeEmulators.h" -#include "GamelistFileParse.h" +#include "GamelistFileParser.h" #include "SystemData.h" #include "views/ViewController.h" @@ -160,7 +160,7 @@ void GuiAlternativeEmulators::selectorWindow(SystemData* system) system->setAlternativeEmulator(""); else system->setAlternativeEmulator(entry.second); - updateGamelist(system, true); + GamelistFileParser::updateGamelist(system, true); if (entry.second == system->getSystemEnvData()->mLaunchCommands.front().second) { if (system->getSystemEnvData()->mLaunchCommands.front().second == "") { diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 229153315..3adf0f142 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -13,7 +13,7 @@ #include "CollectionSystemsManager.h" #include "FileData.h" #include "FileFilterIndex.h" -#include "GamelistFileParse.h" +#include "GamelistFileParser.h" #include "MameNames.h" #include "SystemData.h" #include "Window.h" @@ -675,7 +675,7 @@ void GuiMetaDataEd::save() // not really good as the gamelist will be written twice, but it's a very special and // hopefully rare situation. if (hideGameWhileHidden) - updateGamelist(mScraperParams.system); + GamelistFileParser::updateGamelist(mScraperParams.system); // Enter game in index. mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index f80568ede..f16b84af2 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -13,7 +13,7 @@ #include "CollectionSystemsManager.h" #include "FileFilterIndex.h" -#include "GamelistFileParse.h" +#include "GamelistFileParser.h" #include "MameNames.h" #include "SystemData.h" #include "Window.h" @@ -273,7 +273,7 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result) search.system->getIndex()->removeFromIndex(search.game); GuiScraperSearch::saveMetadata(result, search.game->metadata, search.game); - updateGamelist(search.system); + GamelistFileParser::updateGamelist(search.system); search.system->getIndex()->addToIndex(search.game);