ES-DE/es-app/src/SystemData.cpp
Leon Styhre a233b96c2a Removed some unnecessary typedefs and replaced the remaining ones with the more modern 'using' keyword.
Also harmonized the names of some user defined types and made some other minor cleanup.
2022-01-11 21:57:00 +01:00

1287 lines
50 KiB
C++

// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// 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 "Gamelist.h"
#include "Log.h"
#include "Settings.h"
#include "ThemeData.h"
#include "resources/ResourceManager.h"
#include "utils/FileSystemUtil.h"
#include "utils/StringUtil.h"
#include "views/UIModeController.h"
#include "views/ViewController.h"
#include "views/gamelist/IGameListView.h"
#include <fstream>
#include <pugixml.hpp>
#include <random>
std::vector<SystemData*> SystemData::sSystemVector;
std::unique_ptr<FindRules> SystemData::sFindRules;
FindRules::FindRules()
{
LOG(LogInfo) << "Loading emulator find rules...";
loadFindRules();
}
void FindRules::loadFindRules()
{
std::string customSystemsDirectory =
Utils::FileSystem::getHomePath() + "/.emulationstation/custom_systems";
std::string path = customSystemsDirectory + "/es_find_rules.xml";
if (Utils::FileSystem::exists(path)) {
LOG(LogInfo) << "Found custom find rules configuration file";
}
else {
#if defined(_WIN64)
path = ResourceManager::getInstance().getResourcePath(":/systems/windows/es_find_rules.xml",
false);
#elif defined(__APPLE__)
path = ResourceManager::getInstance().getResourcePath(":/systems/macos/es_find_rules.xml",
false);
#else
path = ResourceManager::getInstance().getResourcePath(":/systems/unix/es_find_rules.xml",
false);
#endif
}
if (path == "") {
LOG(LogWarning) << "No find rules configuration file found";
return;
}
LOG(LogInfo) << "Parsing find rules configuration file \"" << path << "\"...";
pugi::xml_document doc;
#if defined(_WIN64)
pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str());
#else
pugi::xml_parse_result res = doc.load_file(path.c_str());
#endif
if (!res) {
LOG(LogError) << "Couldn't parse es_find_rules.xml: " << res.description();
return;
}
// Actually read the file.
pugi::xml_node ruleList = doc.child("ruleList");
if (!ruleList) {
LOG(LogError) << "es_find_rules.xml is missing the <ruleList> tag";
return;
}
EmulatorRules emulatorRules;
CoreRules coreRules;
for (pugi::xml_node emulator = ruleList.child("emulator"); emulator;
emulator = emulator.next_sibling("emulator")) {
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()) {
LOG(LogWarning) << "Found repeating emulator tag \"" << emulatorName
<< "\", skipping entry";
continue;
}
for (pugi::xml_node rule = emulator.child("rule"); rule; rule = rule.next_sibling("rule")) {
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") {
#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")) {
std::string entryValue = entry.text().get();
if (ruleType == "systempath")
emulatorRules.systemPaths.push_back(entryValue);
else if (ruleType == "staticpath")
emulatorRules.staticPaths.push_back(entryValue);
#if defined(_WIN64)
else if (ruleType == "winregistrypath")
emulatorRules.winRegistryPaths.push_back(entryValue);
else if (ruleType == "winregistryvalue")
emulatorRules.winRegistryValues.push_back(entryValue);
#endif
}
}
mEmulators[emulatorName] = emulatorRules;
emulatorRules.systemPaths.clear();
emulatorRules.staticPaths.clear();
#if defined(_WIN64)
emulatorRules.winRegistryPaths.clear();
emulatorRules.winRegistryValues.clear();
#endif
}
for (pugi::xml_node core = ruleList.child("core"); core; core = core.next_sibling("core")) {
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()) {
LOG(LogWarning) << "Found repeating core tag \"" << coreName << "\", skipping entry";
continue;
}
for (pugi::xml_node rule = core.child("rule"); rule; rule = rule.next_sibling("rule")) {
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")) {
std::string entryValue = entry.text().get();
if (ruleType == "corepath")
coreRules.corePaths.push_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)
, mIsCollectionSystem(CollectionSystem)
, mIsCustomCollectionSystem(CustomCollectionSystem)
, mIsGroupedCustomCollectionSystem(false)
, mIsGameSystem(true)
, mScrapeFlag(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"))
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();
}
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()
{
// We exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least).
// If/when there are more in the future, maybe this can be a more complex method, with a proper
// list but for now a simple string comparison is enough.
mIsGameSystem = (mName != "retropie");
}
bool SystemData::populateFolder(FileData* folder)
{
const std::string& folderPath = folder->getPath();
std::string filePath;
std::string extension;
bool isGame;
bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles");
Utils::FileSystem::StringList dirContent = Utils::FileSystem::getDirContent(folderPath);
// If system directory exists but contains no games, return as error.
if (dirContent.size() == 0)
return false;
for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin();
it != dirContent.cend(); ++it) {
filePath = *it;
// 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 "
<< (Utils::FileSystem::isDirectory(filePath) ? "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()) {
FileData* newGame = new FileData(GAME, filePath, mEnvData, this);
// If adding a configured file extension to a directory it will get interpreted as
// a regular file. This is useful for some 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 (Utils::FileSystem::isDirectory(filePath)) {
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 && Utils::FileSystem::isDirectory(filePath)) {
// Make sure that it's not a recursive symlink pointing to a location higher in the
// hierarchy as the application would run forever trying to resolve the link.
if (Utils::FileSystem::isSymlink(filePath)) {
const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(filePath);
const std::string canonicalStartPath =
Utils::FileSystem::getCanonicalPath(mEnvData->mStartPath);
const std::string combinedPath =
mEnvData->mStartPath +
canonicalPath.substr(canonicalStartPath.size(),
canonicalStartPath.size() - canonicalPath.size());
if (filePath.find(combinedPath) == 0) {
LOG(LogWarning) << "Skipped \"" << filePath << "\" as it's a recursive symlink";
continue;
}
}
FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this);
populateFolder(newFolder);
// 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<FileData*>& children = folder->getChildren();
for (std::vector<FileData*>::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<std::string> readList(const std::string& str, const std::string& delims = " \t\r\n,")
{
std::vector<std::string> 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.push_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<FindRules>();
LOG(LogInfo) << "Populating game systems...";
std::vector<std::string> configPaths = getConfigPath(true);
const std::string rompath = FileData::getROMDirectory();
bool onlyProcessCustomFile = false;
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;
LOG(LogInfo) << "Parsing systems configuration file \"" << configPath << "\"...";
pugi::xml_document doc;
#if defined(_WIN64)
pugi::xml_parse_result res =
doc.load_file(Utils::String::stringToWideString(configPath).c_str());
#else
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;
}
pugi::xml_node loadExclusive = doc.child("loadExclusive");
if (loadExclusive) {
if (configPath == configPaths.front() && configPaths.size() > 1) {
LOG(LogInfo) << "Only loading custom file as the <loadExclusive> tag is present";
onlyProcessCustomFile = true;
}
else {
LOG(LogWarning) << "A <loadExclusive> 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.
pugi::xml_node systemList = doc.child("systemList");
if (!systemList) {
LOG(LogError) << "es_systems.xml is missing the <systemList> tag";
return true;
}
for (pugi::xml_node system = systemList.child("system"); system;
system = system.next_sibling("system")) {
std::string name;
std::string fullname;
std::string sortName;
std::string path;
std::string themeFolder;
name = system.child("name").text().get();
fullname = system.child("fullname").text().get();
sortName = system.child("systemsortname").text().get();
path = system.child("path").text().get();
auto nameFindFunc = [&] {
for (auto system : sSystemVector) {
if (system->mName == name) {
LOG(LogWarning) << "A system with the name \"" << name
<< "\" has already been loaded, skipping duplicate entry";
return true;
}
}
return false;
};
// If the name is matching a system that has already been loaded, then skip the entry.
if (nameFindFunc())
continue;
// If there is a %ROMPATH% variable set for the system, expand it. By doing this
// it's possible to use either absolute ROM paths in es_systems.xml or to utilize
// the ROM path configured as ROMDirectory in es_settings.xml. If it's set to ""
// in this configuration file, the default hardcoded path $HOME/ROMs/ will be used.
path = Utils::String::replace(path, "%ROMPATH%", rompath);
#if defined(_WIN64)
path = Utils::String::replace(path, "\\", "/");
#endif
path = Utils::String::replace(path, "//", "/");
// Check that the ROM directory for the system is valid or otherwise abort the
// processing.
if (!Utils::FileSystem::exists(path)) {
LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name
<< "\" as the defined ROM directory \"" << path
<< "\" does not exist";
continue;
}
if (!Utils::FileSystem::isDirectory(path)) {
LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name
<< "\" as the defined ROM directory \"" << path
<< "\" is not actually a directory";
continue;
}
if (Utils::FileSystem::isSymlink(path)) {
// Make sure that the symlink is not pointing to somewhere higher in the hierarchy
// as that would lead to an infite loop, meaning the application would never start.
std::string resolvedRompath = Utils::FileSystem::getCanonicalPath(rompath);
if (resolvedRompath.find(Utils::FileSystem::getCanonicalPath(path)) == 0) {
LOG(LogWarning)
<< "Skipping system \"" << name << "\" as the defined ROM directory \""
<< path << "\" is an infinitely recursive symlink";
continue;
}
}
// Convert extensions list from a string into a vector of strings.
std::vector<std::string> 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<std::pair<std::string, std::string>> 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;
}
commands.push_back(
std::make_pair(entry.text().get(), entry.attribute("label").as_string()));
}
// Platform ID list
const std::string platformList =
Utils::String::toLower(system.child("platform").text().get());
if (platformList == "") {
LOG(LogWarning) << "No platform defined for system \"" << name
<< "\", scraper searches will be inaccurate";
}
std::vector<std::string> platformStrs = readList(platformList);
std::vector<PlatformIds::PlatformId> platformIds;
for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); ++it) {
std::string str = *it;
PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str);
if (platformId == PlatformIds::PLATFORM_IGNORE) {
// When platform is PLATFORM_IGNORE, do not allow other platforms.
platformIds.clear();
platformIds.push_back(platformId);
break;
}
// If there's a platform entry defined but it does not match the list of supported
// platforms, then generate a warning.
if (str != "" && platformId == PlatformIds::PLATFORM_UNKNOWN)
LOG(LogWarning) << "Unknown platform \"" << str << "\" defined for system \""
<< name << "\", scraper searches will be inaccurate";
else if (platformId != PlatformIds::PLATFORM_UNKNOWN)
platformIds.push_back(platformId);
}
// 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;
}
else {
LOG(LogDebug) << "SystemData::loadConfig(): System \"" << name
<< "\" has a <systemsortname> tag set, sorting as \"" << sortName
<< "\" instead of \"" << 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<FileData*> recursiveGames =
newSys->getRootFolder()->getChildrenRecursive();
onlyHidden = true;
for (auto it = recursiveGames.cbegin(); it != recursiveGames.cend(); ++it) {
if ((*it)->getType() != FOLDER) {
onlyHidden = (*it)->getHidden();
if (!onlyHidden)
break;
}
}
}
if (newSys->getRootFolder()->getChildrenByFilename().size() == 0 || onlyHidden) {
LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name
<< "\" as no files matched any of the defined file extensions";
delete newSys;
}
else {
sSystemVector.push_back(newSys);
}
}
}
// Sort systems by sortName, which will normally be the same as the full name.
std::sort(std::begin(sSystemVector), std::end(sSystemVector),
[](SystemData* a, SystemData* b) { return a->getSortName() < b->getSortName(); });
// Don't load any collections if there are no systems available.
if (sSystemVector.size() > 0)
CollectionSystemsManager::getInstance()->loadCollectionSystems();
return false;
}
std::string SystemData::getLaunchCommandFromLabel(const std::string& label)
{
auto commandIter = std::find_if(
mEnvData->mLaunchCommands.cbegin(), mEnvData->mLaunchCommands.cend(),
[label](std::pair<std::string, std::string> 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<std::string> SystemData::getConfigPath(bool legacyWarning)
{
std::vector<std::string> paths;
if (legacyWarning) {
std::string legacyConfigFile =
Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg";
if (Utils::FileSystem::exists(legacyConfigFile)) {
LOG(LogInfo) << "Found legacy systems configuration file \"" << legacyConfigFile
<< "\", to retain your customizations move it to "
"\"custom_systems/es_systems.xml\" or otherwise delete the file";
}
}
std::string customSystemsDirectory =
Utils::FileSystem::getHomePath() + "/.emulationstation/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.push_back(path);
}
#if 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);
#else
path = ResourceManager::getInstance().getResourcePath(":/systems/unix/es_systems.xml", true);
#endif
paths.push_back(path);
return paths;
}
bool SystemData::createSystemDirectories()
{
std::vector<std::string> configPaths = getConfigPath(true);
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)) {
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) << "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)
pugi::xml_parse_result res =
doc.load_file(Utils::String::stringToWideString(configPaths.front()).c_str());
#else
pugi::xml_parse_result res = doc.load_file(configPaths.front().c_str());
#endif
if (res) {
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
// <path> tags will be overwritten by the last occurrence.
std::reverse(configPaths.begin(), configPaths.end());
std::vector<std::pair<std::string, std::string>> systemsVector;
for (auto configPath : configPaths) {
// If the loadExclusive tag is present.
if (onlyProcessCustomFile && configPath == configPaths.front())
continue;
LOG(LogInfo) << "Parsing systems configuration file \"" << configPath << "\"...";
pugi::xml_document doc;
#if defined(_WIN64)
pugi::xml_parse_result res =
doc.load_file(Utils::String::stringToWideString(configPath).c_str());
#else
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.
pugi::xml_node systemList = doc.child("systemList");
if (!systemList) {
LOG(LogError) << "es_systems.xml is missing the <systemList> 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<std::string> commands;
std::string platform;
std::string themeFolder;
const std::string systemInfoFileName = "/systeminfo.txt";
bool replaceInfoFile = false;
std::ofstream systemInfoFile;
name = system.child("name").text().get();
fullname = system.child("fullname").text().get();
path = system.child("path").text().get();
extensions = system.child("extension").text().get();
for (pugi::xml_node entry = system.child("command"); entry;
entry = entry.next_sibling("command")) {
commands.push_back(entry.text().get());
}
platform = Utils::String::toLower(system.child("platform").text().get());
themeFolder = system.child("theme").text().as_string(name.c_str());
// Check that the %ROMPATH% variable is actually used for the path element.
// If not, skip the system.
if (path.find("%ROMPATH%") != 0) {
LOG(LogWarning) << "The path element for system \"" << name
<< "\" does not "
"utilize the %ROMPATH% variable, skipping entry";
continue;
}
else {
systemDir = path.substr(9, path.size() - 9);
}
// Trim any leading directory separator characters.
systemDir.erase(systemDir.begin(),
std::find_if(systemDir.begin(), systemDir.end(),
[](char c) { return c != '/' && c != '\\'; }));
if (!Utils::FileSystem::exists(rompath + systemDir)) {
if (!Utils::FileSystem::createDirectory(rompath + systemDir)) {
LOG(LogError) << "Couldn't create system directory \"" << systemDir
<< "\", permission problems or disk full?";
return true;
}
else {
LOG(LogInfo) << "Created system directory \"" << systemDir << "\"";
}
}
else {
LOG(LogInfo) << "System directory \"" << systemDir << "\" already exists";
}
if (Utils::FileSystem::exists(rompath + systemDir + systemInfoFileName))
replaceInfoFile = true;
else
replaceInfoFile = false;
if (replaceInfoFile) {
if (Utils::FileSystem::removeFile(rompath + systemDir + systemInfoFileName))
return true;
}
#if defined(_WIN64)
systemInfoFile.open(
Utils::String::stringToWideString(rompath + systemDir + systemInfoFileName)
.c_str());
#else
systemInfoFile.open(rompath + systemDir + systemInfoFileName);
#endif
if (systemInfoFile.fail()) {
LOG(LogError) << "Couldn't create system information file \""
<< rompath + systemDir + systemInfoFileName
<< "\", permission problems or disk full?";
systemInfoFile.close();
return true;
}
systemInfoFile << "System name:" << std::endl;
if (configPaths.size() != 1 && configPath == configPaths.back())
systemInfoFile << name << " (custom system)" << std::endl << std::endl;
else
systemInfoFile << name << std::endl << std::endl;
systemInfoFile << "Full system name:" << std::endl;
systemInfoFile << fullname << std::endl << std::endl;
systemInfoFile << "Supported file extensions:" << std::endl;
systemInfoFile << extensions << std::endl << std::endl;
systemInfoFile << "Launch command:" << std::endl;
systemInfoFile << commands.front() << std::endl << std::endl;
// Alternative emulator configuration entries.
if (commands.size() > 1) {
systemInfoFile << (commands.size() == 2 ? "Alternative launch command:" :
"Alternative launch commands:")
<< std::endl;
for (auto it = commands.cbegin() + 1; it != commands.cend(); ++it)
systemInfoFile << (*it) << std::endl;
systemInfoFile << std::endl;
}
systemInfoFile << "Platform (for scraping):" << std::endl;
systemInfoFile << platform << std::endl << std::endl;
systemInfoFile << "Theme folder:" << std::endl;
systemInfoFile << themeFolder << std::endl;
systemInfoFile.close();
auto systemIter = std::find_if(systemsVector.cbegin(), systemsVector.cend(),
[systemDir](std::pair<std::string, std::string> system) {
return system.first == systemDir;
});
if (systemIter != systemsVector.cend())
systemsVector.erase(systemIter);
if (configPaths.size() != 1 && configPath == configPaths.back())
systemsVector.push_back(std::make_pair(systemDir + " (custom system)", fullname));
else
systemsVector.push_back(std::make_pair(systemDir, fullname));
if (replaceInfoFile) {
LOG(LogInfo) << "Replaced existing system information file \""
<< rompath + systemDir + systemInfoFileName << "\"";
}
else {
LOG(LogInfo) << "Created system information file \""
<< rompath + systemDir + systemInfoFileName << "\"";
}
}
}
// 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;
}
const bool SystemData::isVisible() const
{
// This function doesn't make much sense at the moment; if a system does not have any
// games available, it will not be processed during startup and will as such not exist.
// In the future this function may be used for an option to hide specific systems, but
// for the time being all systems will always be visible.
return true;
}
SystemData* SystemData::getSystemByName(const std::string& systemName)
{
for (auto it : sSystemVector) {
if ((*it).getName() == systemName)
return it;
}
return nullptr;
}
SystemData* SystemData::getNext() const
{
std::vector<SystemData*>::const_iterator it = getIterator();
// As we are starting in a valid gamelistview, this will
// always succeed, even if we have to come full circle.
do {
++it;
if (it == sSystemVector.cend())
it = sSystemVector.cbegin();
} while (!(*it)->isVisible());
return *it;
}
SystemData* SystemData::getPrev() const
{
std::vector<SystemData*>::const_reverse_iterator it = getRevIterator();
// As we are starting in a valid gamelistview, this will
// always succeed, even if we have to come full circle.
do {
++it;
if (it == sSystemVector.crend())
it = sSystemVector.crbegin();
} while (!(*it)->isVisible());
return *it;
}
std::string SystemData::getGamelistPath(bool forWrite) const
{
std::string filePath;
filePath = mRootFolder->getPath() + "/gamelist.xml";
if (Utils::FileSystem::exists(filePath))
return filePath;
filePath = Utils::FileSystem::getHomePath() + "/.emulationstation/gamelists/" + mName +
"/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
{
// Locations where we check for themes, in the following order:
// 1. [SYSTEM_PATH]/theme.xml
// 2. System theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml
// 3. Default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml
// First, check game folder.
std::string localThemePath = mRootFolder->getPath() + "/theme.xml";
if (Utils::FileSystem::exists(localThemePath))
return localThemePath;
// Not in game folder, try system theme in theme sets.
localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder);
if (Utils::FileSystem::exists(localThemePath))
return localThemePath;
// Not system theme, try default system theme in theme set.
localThemePath =
Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml";
return localThemePath;
}
SystemData* SystemData::getRandomSystem(const SystemData* currentSystem)
{
unsigned 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<int> 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)
{
std::vector<FileData*> 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 {
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;
unsigned int total = static_cast<int>(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<int> 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;
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) {
IGameListView* gameList = ViewController::getInstance()->getGameListView(this).get();
gameList->setCursor(gameList->getFirstEntry());
}
}
std::pair<unsigned int, unsigned int> 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()
{
mTheme = std::make_shared<ThemeData>();
std::string path = getThemePath();
if (!Utils::FileSystem::exists(path)) // No theme available for this platform.
return;
try {
// Build map with system variables for theme to use.
std::map<std::string, std::string> sysData;
sysData.insert(std::pair<std::string, std::string>("system.name", getName()));
sysData.insert(std::pair<std::string, std::string>("system.theme", getThemeFolder()));
sysData.insert(std::pair<std::string, std::string>("system.fullName", getFullName()));
mTheme->loadFile(sysData, path);
}
catch (ThemeException& e) {
LOG(LogError) << e.what();
mTheme = std::make_shared<ThemeData>(); // Reset to empty.
}
}
void SystemData::writeMetaData()
{
if (Settings::getInstance()->getBool("IgnoreGamelist") || mIsCollectionSystem)
return;
// Save changed game data back to xml.
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"));
}