2020-09-21 16:13:27 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-05-24 08:29:29 +00:00
|
|
|
//
|
2020-09-21 16:13:27 +00:00
|
|
|
// EmulationStation Desktop Edition
|
2020-06-21 12:25:28 +00:00
|
|
|
// SystemData.cpp
|
2020-05-24 08:29:29 +00:00
|
|
|
//
|
2020-06-21 12:25:28 +00:00
|
|
|
// Provides data structures for the game systems and populates and indexes them based
|
|
|
|
// on the configuration in es_systems.cfg 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.
|
2020-05-24 08:29:29 +00:00
|
|
|
//
|
|
|
|
|
2014-06-25 16:29:58 +00:00
|
|
|
#include "SystemData.h"
|
2017-11-01 22:21:10 +00:00
|
|
|
|
2020-07-09 17:26:48 +00:00
|
|
|
#include "resources/ResourceManager.h"
|
2018-01-09 22:55:09 +00:00
|
|
|
#include "utils/FileSystemUtil.h"
|
2020-06-18 15:09:32 +00:00
|
|
|
#include "utils/StringUtil.h"
|
2020-09-21 16:13:27 +00:00
|
|
|
#include "views/UIModeController.h"
|
2020-09-26 11:03:14 +00:00
|
|
|
#include "views/ViewController.h"
|
2017-11-01 22:21:10 +00:00
|
|
|
#include "CollectionSystemManager.h"
|
|
|
|
#include "FileFilterIndex.h"
|
|
|
|
#include "FileSorts.h"
|
2014-06-25 16:29:58 +00:00
|
|
|
#include "Gamelist.h"
|
|
|
|
#include "Log.h"
|
2020-06-21 10:26:21 +00:00
|
|
|
#include "Platform.h"
|
2014-06-25 16:29:58 +00:00
|
|
|
#include "Settings.h"
|
2017-11-01 22:21:10 +00:00
|
|
|
#include "ThemeData.h"
|
2020-07-03 18:23:51 +00:00
|
|
|
|
2017-11-01 22:21:10 +00:00
|
|
|
#include <fstream>
|
2020-09-21 16:13:27 +00:00
|
|
|
#include <pugixml.hpp>
|
2020-10-21 19:56:31 +00:00
|
|
|
#include <random>
|
2014-06-25 16:29:58 +00:00
|
|
|
|
|
|
|
std::vector<SystemData*> SystemData::sSystemVector;
|
|
|
|
|
2020-05-24 08:29:29 +00:00
|
|
|
SystemData::SystemData(
|
2020-06-21 12:25:28 +00:00
|
|
|
const std::string& name,
|
|
|
|
const std::string& fullName,
|
|
|
|
SystemEnvironmentData* envData,
|
|
|
|
const std::string& themeFolder,
|
2020-09-21 16:13:27 +00:00
|
|
|
bool CollectionSystem,
|
|
|
|
bool CustomCollectionSystem)
|
2020-06-21 12:25:28 +00:00
|
|
|
: mName(name),
|
|
|
|
mFullName(fullName),
|
|
|
|
mEnvData(envData),
|
|
|
|
mThemeFolder(themeFolder),
|
|
|
|
mIsCollectionSystem(CollectionSystem),
|
2020-09-21 16:13:27 +00:00
|
|
|
mIsCustomCollectionSystem(CustomCollectionSystem),
|
2020-10-30 09:12:15 +00:00
|
|
|
mIsGroupedCustomCollectionSystem(false),
|
2020-06-21 12:25:28 +00:00
|
|
|
mIsGameSystem(true),
|
|
|
|
mScrapeFlag(false)
|
2014-06-25 16:29:58 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
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);
|
|
|
|
|
2020-06-21 20:11:29 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
if (!Settings::getInstance()->getBool("IgnoreGamelist"))
|
|
|
|
parseGamelist(this);
|
|
|
|
|
|
|
|
setupSystemSortType(mRootFolder);
|
|
|
|
|
2020-09-17 20:18:13 +00:00
|
|
|
mRootFolder->sort(mRootFolder->getSortTypeFromString(mRootFolder->getSortTypeString()),
|
2020-06-21 12:25:28 +00:00
|
|
|
Settings::getInstance()->getBool("FavoritesFirst"));
|
|
|
|
|
|
|
|
indexAllGameFilters(mRootFolder);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Virtual systems are updated afterwards by CollectionSystemManager.
|
|
|
|
// We're just creating the data structure here.
|
|
|
|
mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this);
|
|
|
|
setupSystemSortType(mRootFolder);
|
|
|
|
}
|
|
|
|
setIsGameSystemStatus();
|
|
|
|
loadTheme();
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
SystemData::~SystemData()
|
|
|
|
{
|
2020-10-18 17:18:02 +00:00
|
|
|
if (Settings::getInstance()->getString("SaveGamelistsMode") == "on exit") {
|
|
|
|
if (mRootFolder->getGameCount().first + mRootFolder->getGameCount().second != 0)
|
|
|
|
writeMetaData();
|
|
|
|
}
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
delete mRootFolder;
|
|
|
|
delete mFilterIndex;
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2017-06-12 16:38:59 +00:00
|
|
|
void SystemData::setIsGameSystemStatus()
|
2014-06-25 16:29:58 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
// 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 more performant.
|
|
|
|
mIsGameSystem = (mName != "retropie");
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2020-06-21 20:11:29 +00:00
|
|
|
bool SystemData::populateFolder(FileData* folder)
|
2014-06-25 16:29:58 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
const std::string& folderPath = folder->getPath();
|
2020-07-08 15:01:47 +00:00
|
|
|
if (!Utils::FileSystem::exists(folderPath)) {
|
2020-07-12 10:09:01 +00:00
|
|
|
LOG(LogDebug) << "SystemData::populateFolder(): Folder with path \"" <<
|
2020-10-19 15:28:20 +00:00
|
|
|
folderPath << "\" does not exist";
|
2020-07-08 15:01:47 +00:00
|
|
|
return false;
|
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!Utils::FileSystem::isDirectory(folderPath)) {
|
2020-07-26 21:30:45 +00:00
|
|
|
LOG(LogWarning) << "Folder with path \"" <<
|
2020-10-19 15:28:20 +00:00
|
|
|
folderPath << "\" is not a directory";
|
2020-06-21 20:11:29 +00:00
|
|
|
return false;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure that this isn't a symlink to an object we already have.
|
|
|
|
if (Utils::FileSystem::isSymlink(folderPath)) {
|
|
|
|
// If this symlink resolves to somewhere that's at the beginning of our
|
|
|
|
// path, it's going to create a recursive loop. Make sure to avoid this.
|
|
|
|
if (folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) {
|
2020-07-26 21:30:45 +00:00
|
|
|
LOG(LogWarning) << "Skipping infinitely recursive symlink \"" <<
|
2020-07-08 15:01:47 +00:00
|
|
|
folderPath << "\"";
|
2020-06-21 20:11:29 +00:00
|
|
|
return false;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string filePath;
|
|
|
|
std::string extension;
|
|
|
|
bool isGame;
|
2020-07-26 11:58:49 +00:00
|
|
|
bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles");
|
2020-06-21 12:25:28 +00:00
|
|
|
Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath);
|
2020-06-21 20:11:29 +00:00
|
|
|
|
2020-06-22 17:46:09 +00:00
|
|
|
// If system directory exists but contains no games, return as error.
|
2020-06-21 20:11:29 +00:00
|
|
|
if (dirContent.size() == 0)
|
|
|
|
return false;
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin();
|
|
|
|
it != dirContent.cend(); ++it) {
|
|
|
|
filePath = *it;
|
|
|
|
|
|
|
|
// Skip hidden files and folders.
|
2020-10-19 15:28:20 +00:00
|
|
|
if (!showHiddenFiles && Utils::FileSystem::isHidden(filePath)) {
|
|
|
|
LOG(LogDebug) << "SystemData::populateFolder(): Skipping hidden " <<
|
|
|
|
(Utils::FileSystem::isDirectory(filePath) ? "directory \"" : "file \"") <<
|
|
|
|
filePath << "\"";
|
2020-06-21 12:25:28 +00:00
|
|
|
continue;
|
2020-10-19 15:28:20 +00:00
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
// FYI, folders *can* also match the extension and be added as games.
|
|
|
|
// This is mostly just to support higan.
|
|
|
|
// See issue #75: https://github.com/Aloshi/EmulationStation/issues/75
|
|
|
|
|
|
|
|
isGame = false;
|
|
|
|
if (std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(),
|
|
|
|
extension) != mEnvData->mSearchExtensions.cend()) {
|
|
|
|
FileData* newGame = new FileData(GAME, filePath, mEnvData, this);
|
|
|
|
|
|
|
|
// Prevent new arcade assets from being added.
|
|
|
|
if (!newGame->isArcadeAsset()) {
|
|
|
|
folder->addChild(newGame);
|
|
|
|
isGame = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add directories that also do not match an extension as folders.
|
|
|
|
if (!isGame && Utils::FileSystem::isDirectory(filePath)) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2020-06-21 20:11:29 +00:00
|
|
|
return true;
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2017-11-04 16:03:24 +00:00
|
|
|
void SystemData::indexAllGameFilters(const FileData* folder)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
const std::vector<FileData*>& children = folder->getChildren();
|
|
|
|
|
|
|
|
for (std::vector<FileData*>::const_iterator it = children.cbegin();
|
|
|
|
it != children.cend(); ++it) {
|
|
|
|
switch ((*it)->getType()) {
|
|
|
|
case GAME: {
|
|
|
|
mFilterIndex->addToIndex(*it);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case FOLDER: {
|
|
|
|
indexAllGameFilters(*it);
|
|
|
|
}
|
|
|
|
break;
|
2020-06-25 17:52:38 +00:00
|
|
|
default:
|
|
|
|
break;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
2017-11-04 16:03:24 +00:00
|
|
|
}
|
|
|
|
|
2014-06-25 16:29:58 +00:00
|
|
|
std::vector<std::string> readList(const std::string& str, const char* delims = " \t\r\n,")
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::vector<std::string> ret;
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
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));
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
prevOff = str.find_first_not_of(delims, off);
|
|
|
|
off = str.find_first_of(delims, prevOff);
|
|
|
|
}
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return ret;
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 08:29:29 +00:00
|
|
|
// Creates systems from information located in a config file.
|
2014-06-25 16:29:58 +00:00
|
|
|
bool SystemData::loadConfig()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
deleteSystems();
|
|
|
|
|
|
|
|
std::string path = getConfigPath(false);
|
2020-10-19 15:28:20 +00:00
|
|
|
const std::string rompath = FileData::getROMDirectory();
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
if (!Utils::FileSystem::exists(path)) {
|
2020-10-19 15:28:20 +00:00
|
|
|
LOG(LogWarning) << "Systems configuration file does not exist";
|
2020-07-09 17:26:48 +00:00
|
|
|
if (copyConfigTemplate(getConfigPath(true)))
|
|
|
|
return false;
|
|
|
|
path = getConfigPath(false);
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
|
2020-10-19 15:28:20 +00:00
|
|
|
LOG(LogInfo) << "Parsing systems configuration file \"" << path << "\"...";
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_document doc;
|
2020-08-23 15:04:30 +00:00
|
|
|
#if defined(_WIN64)
|
2020-07-10 16:32:23 +00:00
|
|
|
pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str());
|
|
|
|
#else
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_parse_result res = doc.load_file(path.c_str());
|
2020-07-10 16:32:23 +00:00
|
|
|
#endif
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
if (!res) {
|
2020-07-26 21:30:45 +00:00
|
|
|
LOG(LogError) << "Could not parse es_systems.cfg";
|
2020-06-21 12:25:28 +00:00
|
|
|
LOG(LogError) << res.description();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Actually read the file.
|
|
|
|
pugi::xml_node systemList = doc.child("systemList");
|
|
|
|
|
|
|
|
if (!systemList) {
|
2020-10-19 15:28:20 +00:00
|
|
|
LOG(LogError) << "es_systems.cfg is missing the <systemList> tag";
|
2020-06-21 12:25:28 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (pugi::xml_node system = systemList.child("system"); system;
|
|
|
|
system = system.next_sibling("system")) {
|
2020-07-07 19:25:15 +00:00
|
|
|
std::string name;
|
|
|
|
std::string fullname;
|
|
|
|
std::string path;
|
|
|
|
std::string cmd;
|
|
|
|
std::string themeFolder;
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
name = system.child("name").text().get();
|
|
|
|
fullname = system.child("fullname").text().get();
|
|
|
|
path = system.child("path").text().get();
|
|
|
|
|
|
|
|
// 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.cfg or to utilize
|
|
|
|
// the ROM path configured as ROMDirectory in es_settings.cfg. 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);
|
|
|
|
path = Utils::String::replace(path, "//", "/");
|
|
|
|
|
|
|
|
// Convert extensions list from a string into a vector of strings.
|
|
|
|
std::vector<std::string> extensions = readList(system.child("extension").text().get());
|
|
|
|
|
|
|
|
cmd = system.child("command").text().get();
|
|
|
|
|
|
|
|
// Platform ID list
|
|
|
|
const char* platformList = system.child("platform").text().get();
|
|
|
|
std::vector<std::string> platformStrs = readList(platformList);
|
|
|
|
std::vector<PlatformIds::PlatformId> platformIds;
|
|
|
|
for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) {
|
|
|
|
const char* str = it->c_str();
|
|
|
|
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 appears to be an actual platform ID supplied
|
|
|
|
// but it didn't match the list, generate a warning.
|
2020-06-23 18:07:00 +00:00
|
|
|
if (str != nullptr && str[0] != '\0' && platformId == PlatformIds::PLATFORM_UNKNOWN)
|
2020-07-26 21:30:45 +00:00
|
|
|
LOG(LogWarning) << "Unknown platform for system \"" << name <<
|
2020-07-08 15:01:47 +00:00
|
|
|
"\" (platform \"" << str << "\" from list \"" << platformList << "\")";
|
2020-06-21 12:25:28 +00:00
|
|
|
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() || path.empty() || extensions.empty() || cmd.empty()) {
|
2020-07-26 21:30:45 +00:00
|
|
|
LOG(LogError) << "System \"" << name <<
|
2020-10-19 15:28:20 +00:00
|
|
|
"\" is missing name, path, extension, or command";
|
2020-06-21 12:25:28 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert path to generic directory seperators.
|
|
|
|
path = Utils::FileSystem::getGenericPath(path);
|
|
|
|
|
2020-08-23 15:04:30 +00:00
|
|
|
#if defined(_WIN64)
|
2020-07-27 11:06:46 +00:00
|
|
|
if (!Settings::getInstance()->getBool("ShowHiddenFiles") &&
|
|
|
|
Utils::FileSystem::isHidden(path)) {
|
|
|
|
LOG(LogWarning) << "Skipping hidden ROM folder " << path;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// Create the system runtime environment data.
|
|
|
|
SystemEnvironmentData* envData = new SystemEnvironmentData;
|
|
|
|
envData->mStartPath = path;
|
|
|
|
envData->mSearchExtensions = extensions;
|
|
|
|
envData->mLaunchCommand = cmd;
|
|
|
|
envData->mPlatformIds = platformIds;
|
|
|
|
|
|
|
|
SystemData* newSys = new SystemData(name, fullname, envData, themeFolder);
|
2020-07-26 20:19:29 +00:00
|
|
|
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 =
|
2020-07-28 13:19:54 +00:00
|
|
|
newSys->getRootFolder()->getChildrenRecursive();
|
2020-07-26 20:19:29 +00:00
|
|
|
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) {
|
2020-07-12 10:09:01 +00:00
|
|
|
LOG(LogDebug) << "SystemData::loadConfig(): System \"" << name <<
|
2020-10-19 15:28:20 +00:00
|
|
|
"\" has no games, ignoring it";
|
2020-06-21 12:25:28 +00:00
|
|
|
delete newSys;
|
2020-10-11 18:54:37 +00:00
|
|
|
delete envData;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
sSystemVector.push_back(newSys);
|
|
|
|
}
|
|
|
|
}
|
2020-09-27 10:49:14 +00:00
|
|
|
|
|
|
|
// Sort systems by their full names.
|
|
|
|
std::sort(std::begin(sSystemVector), std::end(sSystemVector),
|
|
|
|
[](SystemData* a, SystemData* b) {
|
|
|
|
return a->getFullName() < b->getFullName(); });
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// Don't load any collections if there are no systems available.
|
|
|
|
if (sSystemVector.size() > 0)
|
|
|
|
CollectionSystemManager::get()->loadCollectionSystems();
|
|
|
|
|
|
|
|
return true;
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 17:26:48 +00:00
|
|
|
bool SystemData::copyConfigTemplate(const std::string& path)
|
2014-06-25 16:29:58 +00:00
|
|
|
{
|
2020-07-09 17:26:48 +00:00
|
|
|
std::string systemsTemplateFile;;
|
|
|
|
|
2020-10-19 15:28:20 +00:00
|
|
|
LOG(LogInfo) <<
|
|
|
|
"Attempting to copy template es_systems.cfg file from the resources directory...";
|
2020-07-09 17:26:48 +00:00
|
|
|
|
2020-08-19 20:17:32 +00:00
|
|
|
#if defined(_WIN64)
|
2020-07-09 17:26:48 +00:00
|
|
|
systemsTemplateFile = ResourceManager::getInstance()->
|
2020-10-19 15:28:20 +00:00
|
|
|
getResourcePath(":/templates/es_systems.cfg_windows", false);
|
2020-08-19 20:17:32 +00:00
|
|
|
#elif defined(__APPLE__)
|
|
|
|
systemsTemplateFile = ResourceManager::getInstance()->
|
2020-10-19 15:28:20 +00:00
|
|
|
getResourcePath(":/templates/es_systems.cfg_macos", false);
|
2020-08-19 20:17:32 +00:00
|
|
|
#elif defined(__unix__)
|
2020-07-09 17:26:48 +00:00
|
|
|
systemsTemplateFile = ResourceManager::getInstance()->
|
2020-10-19 15:28:20 +00:00
|
|
|
getResourcePath(":/templates/es_systems.cfg_unix", false);
|
2020-07-09 17:26:48 +00:00
|
|
|
#endif
|
|
|
|
|
2020-10-19 15:28:20 +00:00
|
|
|
if (systemsTemplateFile == "") {
|
|
|
|
LOG(LogError) << "Can't find the es_systems.cfg template file";
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (Utils::FileSystem::copyFile(systemsTemplateFile, path, false)) {
|
|
|
|
LOG(LogError) << "Copying of es_systems.cfg template file failed";
|
2020-07-09 17:26:48 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-10-19 15:28:20 +00:00
|
|
|
LOG(LogInfo) << "Template es_systems.cfg file copied successfully";
|
2020-07-09 17:26:48 +00:00
|
|
|
|
|
|
|
return false;
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SystemData::deleteSystems()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
for (unsigned int i = 0; i < sSystemVector.size(); i++)
|
|
|
|
delete sSystemVector.at(i);
|
2020-05-24 08:29:29 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
sSystemVector.clear();
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string SystemData::getConfigPath(bool forWrite)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg";
|
|
|
|
if (forWrite || Utils::FileSystem::exists(path))
|
|
|
|
return path;
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-22 17:46:09 +00:00
|
|
|
return "";
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2017-12-01 19:28:45 +00:00
|
|
|
bool SystemData::isVisible()
|
|
|
|
{
|
2020-09-21 16:13:27 +00:00
|
|
|
// This function doesn't make much sense at the moment; if a game 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;
|
2017-12-01 19:28:45 +00:00
|
|
|
}
|
|
|
|
|
2017-09-08 13:20:07 +00:00
|
|
|
SystemData* SystemData::getNext() const
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::vector<SystemData*>::const_iterator it = getIterator();
|
2017-09-08 13:20:07 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// 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());
|
2017-09-08 13:20:07 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return *it;
|
2017-09-08 13:20:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
SystemData* SystemData::getPrev() const
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::vector<SystemData*>::const_reverse_iterator it = getRevIterator();
|
2017-11-11 14:56:22 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// 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());
|
2017-09-08 13:20:07 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return *it;
|
2017-09-08 13:20:07 +00:00
|
|
|
}
|
|
|
|
|
2014-06-25 16:29:58 +00:00
|
|
|
std::string SystemData::getGamelistPath(bool forWrite) const
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::string filePath;
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
filePath = mRootFolder->getPath() + "/gamelist.xml";
|
|
|
|
if (Utils::FileSystem::exists(filePath))
|
|
|
|
return filePath;
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
filePath = Utils::FileSystem::getHomePath() + "/.emulationstation/gamelists/" +
|
|
|
|
mName + "/gamelist.xml";
|
2020-06-22 17:46:09 +00:00
|
|
|
|
|
|
|
// Make sure the directory exists if we're going to write to it,
|
|
|
|
// or crashes will happen.
|
2020-06-21 12:25:28 +00:00
|
|
|
if (forWrite)
|
|
|
|
Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath));
|
|
|
|
if (forWrite || Utils::FileSystem::exists(filePath))
|
|
|
|
return filePath;
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-22 17:46:09 +00:00
|
|
|
return "";
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string SystemData::getThemePath() const
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
// 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
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// First, check game folder.
|
|
|
|
std::string localThemePath = mRootFolder->getPath() + "/theme.xml";
|
|
|
|
if (Utils::FileSystem::exists(localThemePath))
|
|
|
|
return localThemePath;
|
2014-06-25 16:29:58 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// Not in game folder, try system theme in theme sets.
|
|
|
|
localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder);
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (Utils::FileSystem::exists(localThemePath))
|
|
|
|
return localThemePath;
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// Not system theme, try default system theme in theme set.
|
|
|
|
localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) +
|
|
|
|
"/theme.xml";
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return localThemePath;
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool SystemData::hasGamelist() const
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
return (Utils::FileSystem::exists(getGamelistPath(false)));
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 13:19:54 +00:00
|
|
|
SystemData* SystemData::getRandomSystem(const SystemData* currentSystem)
|
2017-06-12 16:38:59 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
unsigned int total = 0;
|
|
|
|
for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) {
|
|
|
|
if ((*it)->isGameSystem())
|
|
|
|
total++;
|
|
|
|
}
|
|
|
|
|
2020-07-28 13:19:54 +00:00
|
|
|
if (total < 2)
|
|
|
|
return nullptr;
|
|
|
|
|
2020-10-17 12:32:08 +00:00
|
|
|
SystemData* randomSystem = nullptr;
|
2020-07-28 13:19:54 +00:00
|
|
|
|
|
|
|
do {
|
|
|
|
// Get a random number in range.
|
2020-10-21 19:56:31 +00:00
|
|
|
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);
|
2020-07-28 13:19:54 +00:00
|
|
|
|
|
|
|
for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) {
|
|
|
|
if ((*it)->isGameSystem()) {
|
|
|
|
if (target > 0) {
|
|
|
|
target--;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
randomSystem = (*it);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-28 13:19:54 +00:00
|
|
|
while (randomSystem == currentSystem);
|
|
|
|
|
|
|
|
return randomSystem;
|
|
|
|
}
|
|
|
|
|
|
|
|
FileData* SystemData::getRandomGame(const FileData* currentGame)
|
2017-06-12 16:38:59 +00:00
|
|
|
{
|
2020-07-28 13:19:54 +00:00
|
|
|
std::vector<FileData*> gameList = mRootFolder->getFilesRecursive(GAME, true);
|
|
|
|
|
2020-10-21 19:56:31 +00:00
|
|
|
if (!currentGame && gameList.size() == 1)
|
2020-10-20 20:02:33 +00:00
|
|
|
return gameList.front();
|
|
|
|
|
2020-08-06 21:41:44 +00:00
|
|
|
if (currentGame && currentGame->getType() == PLACEHOLDER)
|
|
|
|
return nullptr;
|
|
|
|
|
2020-07-28 13:19:54 +00:00
|
|
|
if (currentGame) {
|
2020-08-06 21:41:44 +00:00
|
|
|
// If the parent of the game only contains folders, then simply make all these
|
|
|
|
// folders the selection list for the random function. This covers folder-only
|
|
|
|
// views regardless of how many levels deep they're located.
|
|
|
|
if (currentGame->getParent()->getOnlyFoldersFlag()) {
|
|
|
|
std::vector<FileData*> childrenList = currentGame->getParent()->getChildren();
|
|
|
|
gameList.erase(gameList.cbegin(), gameList.cend());
|
|
|
|
gameList.reserve(childrenList.size());
|
|
|
|
gameList.insert(gameList.cend(), childrenList.cbegin(), childrenList.cend());
|
|
|
|
}
|
|
|
|
// If the game is inside a folder where there are only other files, update gameList
|
|
|
|
// to only contain these files.
|
|
|
|
else if (currentGame->getParent()->getFullPath() !=
|
2020-07-28 13:19:54 +00:00
|
|
|
currentGame->getSystem()->getRootFolder()->getFullPath()) {
|
|
|
|
std::vector<FileData*> folderList;
|
|
|
|
|
|
|
|
for (auto it = gameList.cbegin(); it != gameList.cend(); it++) {
|
|
|
|
if ((*it)->getParent()->getFullPath() == currentGame->getParent()->getFullPath())
|
|
|
|
folderList.push_back((*it));
|
|
|
|
}
|
|
|
|
|
|
|
|
gameList.erase(gameList.cbegin(), gameList.cend());
|
|
|
|
gameList.reserve(folderList.size());
|
|
|
|
gameList.insert(gameList.cend(), folderList.cbegin(), folderList.cend());
|
|
|
|
}
|
2020-08-06 21:41:44 +00:00
|
|
|
// If the game is inside a folder with a mix of files and folders, then update
|
|
|
|
// gameList to exclude all folders so we don't jump into one of them.
|
2020-07-28 13:19:54 +00:00
|
|
|
else {
|
|
|
|
std::vector<FileData*> childrenList = mRootFolder->getChildren();
|
|
|
|
std::vector<FileData*> noFolderList;
|
|
|
|
|
|
|
|
for (auto it = childrenList.cbegin(); it != childrenList.cend(); it++) {
|
2020-07-28 19:08:17 +00:00
|
|
|
if ((*it)->getType() == GAME || (*it)->getParent()->getOnlyFoldersFlag())
|
2020-07-28 13:19:54 +00:00
|
|
|
noFolderList.push_back((*it));
|
|
|
|
}
|
|
|
|
gameList.erase(gameList.cbegin(), gameList.cend());
|
|
|
|
gameList.reserve(noFolderList.size());
|
|
|
|
gameList.insert(gameList.cend(), noFolderList.cbegin(), noFolderList.cend());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unsigned int total = (int)gameList.size();
|
2020-06-21 12:25:28 +00:00
|
|
|
int target = 0;
|
2020-05-24 08:29:29 +00:00
|
|
|
|
2020-07-28 13:19:54 +00:00
|
|
|
if (total < 2)
|
2020-06-23 18:07:00 +00:00
|
|
|
return nullptr;
|
2020-05-24 08:29:29 +00:00
|
|
|
|
2020-07-28 13:19:54 +00:00
|
|
|
do {
|
|
|
|
// Get a random number in range.
|
2020-10-21 19:56:31 +00:00
|
|
|
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);
|
2020-07-28 13:19:54 +00:00
|
|
|
}
|
|
|
|
while (currentGame && gameList.at(target) == currentGame);
|
|
|
|
|
|
|
|
return gameList.at(target);
|
2017-06-12 16:38:59 +00:00
|
|
|
}
|
|
|
|
|
2020-09-26 11:03:14 +00:00
|
|
|
void SystemData::sortSystem(bool reloadGamelist)
|
|
|
|
{
|
|
|
|
if (getName() == "recent")
|
|
|
|
return;
|
|
|
|
|
|
|
|
bool favoritesSorting;
|
|
|
|
|
2020-10-30 09:12:15 +00:00
|
|
|
if (this->isCustomCollection())
|
2020-09-26 11:03:14 +00:00
|
|
|
favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom");
|
|
|
|
else
|
|
|
|
favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst");
|
|
|
|
|
|
|
|
FileData* rootFolder = getRootFolder();
|
2020-10-21 19:56:31 +00:00
|
|
|
// Assign the sort type to all grouped custom collections.
|
|
|
|
if (mIsCollectionSystem && mFullName == "collections") {
|
|
|
|
for (auto it = rootFolder->getChildren().begin();
|
|
|
|
it != rootFolder->getChildren().end(); it++) {
|
|
|
|
setupSystemSortType((*it)->getSystem()->getRootFolder());
|
|
|
|
}
|
|
|
|
}
|
2020-09-26 11:03:14 +00:00
|
|
|
setupSystemSortType(rootFolder);
|
|
|
|
|
|
|
|
rootFolder->sort(rootFolder->getSortTypeFromString(
|
|
|
|
rootFolder->getSortTypeString()), favoritesSorting);
|
|
|
|
|
|
|
|
if (reloadGamelist)
|
|
|
|
ViewController::get()->reloadGameListView(this, false);
|
|
|
|
}
|
|
|
|
|
2020-09-21 16:13:27 +00:00
|
|
|
std::pair<unsigned int, unsigned int> SystemData::getDisplayedGameCount() const
|
2017-03-18 17:54:39 +00:00
|
|
|
{
|
2020-09-21 16:13:27 +00:00
|
|
|
// 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();
|
2017-03-18 17:54:39 +00:00
|
|
|
}
|
|
|
|
|
2014-06-25 16:29:58 +00:00
|
|
|
void SystemData::loadTheme()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
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.
|
|
|
|
}
|
2014-06-25 16:29:58 +00:00
|
|
|
}
|
2019-08-24 14:22:02 +00:00
|
|
|
|
|
|
|
void SystemData::writeMetaData() {
|
2020-06-21 12:25:28 +00:00
|
|
|
if (Settings::getInstance()->getBool("IgnoreGamelist") || mIsCollectionSystem)
|
|
|
|
return;
|
2019-08-24 14:22:02 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// Save changed game data back to xml.
|
|
|
|
updateGamelist(this);
|
2019-08-24 14:22:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void SystemData::onMetaDataSavePoint() {
|
2020-06-21 12:25:28 +00:00
|
|
|
if (Settings::getInstance()->getString("SaveGamelistsMode") != "always")
|
|
|
|
return;
|
2019-08-24 14:22:02 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
writeMetaData();
|
2019-08-24 14:22:02 +00:00
|
|
|
}
|
2020-05-24 08:29:29 +00:00
|
|
|
|
|
|
|
void SystemData::setupSystemSortType(FileData* mRootFolder)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
// 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")) {
|
|
|
|
mRootFolder->setSortTypeString(Settings::getInstance()->
|
|
|
|
getString("DefaultSortOrder"));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If no valid sort type was defined in the configuration
|
|
|
|
// file, set sorting to "filename, ascending".
|
|
|
|
if (mRootFolder->getSortTypeString() == "")
|
|
|
|
mRootFolder->setSortTypeString(FileSorts::SortTypes.at(0).description);
|
2020-05-24 08:29:29 +00:00
|
|
|
}
|