From 2432e118a71469c9d4c461f3db13604d4a464a99 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 10 Mar 2021 18:21:49 +0100 Subject: [PATCH] Added the ability to automatically generate the game systems directory structure. --- es-app/src/SystemData.cpp | 182 +++++++++++++++++++++++++--- es-app/src/SystemData.h | 8 +- es-app/src/main.cpp | 104 +++------------- es-app/src/views/ViewController.cpp | 114 ++++++++++++++++- es-app/src/views/ViewController.h | 13 +- 5 files changed, 311 insertions(+), 110 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 192ebd82d..fb309a85f 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -220,7 +220,6 @@ std::vector readList(const std::string& str, const std::string& del return ret; } -// Creates systems from information located in a config file. bool SystemData::loadConfig() { deleteSystems(); @@ -231,7 +230,7 @@ bool SystemData::loadConfig() if (!Utils::FileSystem::exists(path)) { LOG(LogInfo) << "Systems configuration file does not exist"; if (copyConfigTemplate(getConfigPath(true))) - return false; + return true; path = getConfigPath(false); } @@ -245,9 +244,9 @@ bool SystemData::loadConfig() #endif if (!res) { - LOG(LogError) << "Could not parse es_systems.cfg"; + LOG(LogError) << "Couldn't parse es_systems.cfg"; LOG(LogError) << res.description(); - return false; + return true; } // Actually read the file. @@ -255,7 +254,7 @@ bool SystemData::loadConfig() if (!systemList) { LOG(LogError) << "es_systems.cfg is missing the tag"; - return false; + return true; } for (pugi::xml_node system = systemList.child("system"); system; @@ -282,12 +281,12 @@ bool SystemData::loadConfig() // 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 game system \"" << + 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 game system \"" << + LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << name << "\" as the defined ROM directory \"" << path << "\" is not actually a directory"; continue; @@ -297,7 +296,7 @@ bool SystemData::loadConfig() // 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 game system \"" << name << + LOG(LogWarning) << "Skipping system \"" << name << "\" as the defined ROM directory \"" << path << "\" is an infinitely recursive symlink"; continue; @@ -387,11 +386,9 @@ bool SystemData::loadConfig() } if (newSys->getRootFolder()->getChildrenByFilename().size() == 0 || onlyHidden) { - LOG(LogWarning) << "No files were found for game system \"" << name << - "\" which matched any of the defined file extensions \"" << - Utils::String::vectorToDelimitedString(extensions, " ") << "\""; + LOG(LogDebug) << "SystemData::loadConfig(): Skipping system \"" << + name << "\" as no files matched any of the defined file extensions"; delete newSys; - delete envData; } else { sSystemVector.push_back(newSys); @@ -407,7 +404,7 @@ bool SystemData::loadConfig() if (sSystemVector.size() > 0) CollectionSystemsManager::get()->loadCollectionSystems(); - return true; + return false; } bool SystemData::copyConfigTemplate(const std::string& path) @@ -459,9 +456,166 @@ std::string SystemData::getConfigPath(bool forWrite) return ""; } +bool SystemData::createSystemDirectories() +{ + std::string path = getConfigPath(false); + const std::string rompath = FileData::getROMDirectory(); + + if (!Utils::FileSystem::exists(path)) { + LOG(LogInfo) << "Systems configuration file does not exist, aborting"; + return true; + } + + 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"; + } + + LOG(LogInfo) << "Parsing systems 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_systems.cfg"; + LOG(LogError) << res.description(); + return true; + } + + // Actually read the file. + pugi::xml_node systemList = doc.child("systemList"); + + if (!systemList) { + LOG(LogError) << "es_systems.cfg is missing the tag"; + return true; + } + + for (pugi::xml_node system = systemList.child("system"); system; + system = system.next_sibling("system")) { + std::string systemDir; + std::string name; + std::string fullname; + std::string path; + std::string extensions; + std::string command; + 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(); + command = system.child("command").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); + #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; + 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 << command << std::endl << 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(); + + if (replaceInfoFile) { + LOG(LogInfo) << "Replaced existing system information file \"" << + rompath + systemDir + systemInfoFileName << "\""; + } + else { + LOG(LogInfo) << "Created system information file \"" << + rompath + systemDir + systemInfoFileName << "\""; + } + } + + LOG(LogInfo) << "System directories successfully created"; + return false; +} + bool SystemData::isVisible() { - // This function doesn't make much sense at the moment; if a game system does not have any + // 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. diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index e519ac4c6..9e1e38eac 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -68,13 +68,15 @@ public: void setScrapeFlag(bool scrapeflag) { mScrapeFlag = scrapeflag; } static void deleteSystems(); - // Load the system config file at getConfigPath(). - // Returns true if no errors were encountered. - // An example will be written if the file doesn't exist. + // Loads the systems configuration file at getConfigPath() and creates the systems. static bool loadConfig(); + static bool copyConfigTemplate(const std::string& path); static std::string getConfigPath(bool forWrite); + // Generates the game system directories and information files based on es_systems.cfg. + static bool createSystemDirectories(); + static std::vector sSystemVector; inline std::vector::const_iterator getIterator() const diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index ff9a57b62..bcc41db68 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -51,7 +51,7 @@ bool forceInputConfig = false; bool settingsNeedSaving = false; -enum returnCode { +enum loadSystemsReturnCode { NO_LOADING_ERROR, NO_SYSTEMS_FILE, NO_ROMS @@ -354,37 +354,16 @@ bool verifyHomeFolderExists() return true; } -// Returns NO_LOADING_ERROR if everything is OK. -// Otherwise returns either NO_SYSTEMS_FILE or NO_ROMS. -returnCode loadSystemConfigFile(std::string& errorMsg) +loadSystemsReturnCode loadSystemConfigFile() { - if (!SystemData::loadConfig()) { - LOG(LogError) << "Could not parse systems configuration file"; - errorMsg = "COULDN'T FIND THE SYSTEMS CONFIGURATION FILE.\n" - "ATTEMPTED TO COPY A TEMPLATE ES_SYSTEMS.CFG FILE\n" - "FROM THE EMULATIONSTATION RESOURCES DIRECTORY,\n" - "BUT THIS FAILED. HAS EMULATIONSTATION BEEN PROPERLY\n" - "INSTALLED AND DO YOU HAVE WRITE PERMISSIONS TO \n" - "YOUR HOME DIRECTORY?"; + if (SystemData::loadConfig()) { + LOG(LogError) << "Could not parse systems configuration file (es_systems.cfg)"; return NO_SYSTEMS_FILE; } if (SystemData::sSystemVector.size() == 0) { - LOG(LogError) << "No systems found, does at least one system have a game present? " - "(Check that the file extensions are supported)"; - errorMsg = "THE SYSTEMS CONFIGURATION FILE EXISTS, BUT NO\n" - "GAME FILES WERE FOUND. EITHER PLACE YOUR GAMES\n" - "IN THE CURRENTLY CONFIGURED ROM DIRECTORY OR\n" - "CHANGE IT USING THE BUTTON BELOW. MAKE SURE\n" - "THAT YOUR FILE EXTENSIONS AND SYSTEMS DIRECTORY\n" - "NAMES ARE SUPPORTED BY EMULATIONSTATION-DE.\n" - "THIS IS THE CURRENTLY CONFIGURED ROM DIRECTORY:\n"; - #if defined(_WIN64) - errorMsg += Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); - #else - errorMsg += FileData::getROMDirectory(); - #endif - + LOG(LogError) << "No game files were found, make sure that the system directories are " + "setup correctly and that the file extensions are supported"; return NO_ROMS; } @@ -493,72 +472,19 @@ int main(int argc, char* argv[]) window.renderLoadingScreen(progressText); } - std::string errorMsg; - returnCode returnCodeValue = loadSystemConfigFile(errorMsg); - - if (returnCodeValue) { - // Something went terribly wrong. - if (errorMsg == "") { - LOG(LogError) << "Unknown error occured while parsing systems configuration file"; - Renderer::deinit(); - return 1; - } + loadSystemsReturnCode loadSystemsStatus = loadSystemConfigFile(); + if (loadSystemsStatus) { // If there was an issue with installing the es_systems.cfg file from the // template directory, then display an error message and let the user quit. // If there are no game files found, give the option to the user to quit or - // to configure a different ROM directory. The application will need to be - // restarted though, to activate any new ROM directory setting. - if (returnCodeValue == NO_SYSTEMS_FILE) { - window.pushGui(new GuiMsgBox(&window, HelpStyle(), - errorMsg.c_str(), - "QUIT", [] { - SDL_Event quit; - quit.type = SDL_QUIT; - SDL_PushEvent(&quit); - }, "", nullptr, "", nullptr, true)); + // to configure a different ROM directory as well as to generate the systems + // directory structure. + if (loadSystemsStatus == NO_SYSTEMS_FILE) { + ViewController::get()->noSystemsFileDialog(); } - else if (returnCodeValue == NO_ROMS) { - auto updateVal = [](const std::string& newROMDirectory) { - Settings::getInstance()->setString("ROMDirectory", newROMDirectory); - Settings::getInstance()->saveFile(); - SDL_Event quit; - quit.type = SDL_QUIT; - SDL_PushEvent(&quit); - }; - - window.pushGui(new GuiMsgBox(&window, HelpStyle(), errorMsg.c_str(), - "CHANGE ROM DIRECTORY", [&window, updateVal] { - std::string currentROMDirectory; - #if defined(_WIN64) - currentROMDirectory = - Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); - #else - currentROMDirectory = FileData::getROMDirectory(); - #endif - - window.pushGui(new GuiComplexTextEditPopup( - &window, - HelpStyle(), - "ENTER ROM DIRECTORY", - "Currently configured directory:", - currentROMDirectory, - currentROMDirectory, - updateVal, - false, - "SAVE AND QUIT", - "SAVE CHANGES?", - "LOAD CURRENT", - "LOAD CURRENTLY CONFIGURED VALUE", - "CLEAR", - "CLEAR (LEAVE BLANK TO RESET TO DEFAULT DIRECTORY)", - true)); - }, - "QUIT", [] { - SDL_Event quit; - quit.type = SDL_QUIT; - SDL_PushEvent(&quit); - }, "", nullptr, true)); + else if (loadSystemsStatus == NO_ROMS) { + ViewController::get()->noGamesDialog(); } } @@ -575,7 +501,7 @@ int main(int argc, char* argv[]) // Choose which GUI to open depending on if an input configuration already exists and // whether the flag to force the input configuration was passed from the command line. - if (errorMsg == "") { + if (!loadSystemsStatus) { if (!forceInputConfig && Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) { ViewController::get()->goToStart(); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index fc1a67d93..803f66a0d 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -4,9 +4,10 @@ // ViewController.cpp // // Handles overall system navigation including animations and transitions. -// Also creates the gamelist views and handles refresh and reloads of these when needed +// Creates the gamelist views and handles refresh and reloads of these when needed // (for example when metadata has been changed or when a list sorting has taken place). // Initiates the launching of games, calling FileData to do the actual launch. +// Displays a dialog when there are no games found on startup. // #include "views/ViewController.h" @@ -16,7 +17,6 @@ #include "animations/MoveCameraAnimation.h" #include "guis/GuiInfoPopup.h" #include "guis/GuiMenu.h" -#include "guis/GuiMsgBox.h" #include "views/gamelist/DetailedGameListView.h" #include "views/gamelist/GridGameListView.h" #include "views/gamelist/IGameListView.h" @@ -72,7 +72,8 @@ ViewController::ViewController( mCancelledTransition(false), mLockInput(false), mNextSystem(false), - mGameToLaunch(nullptr) + mGameToLaunch(nullptr), + mNoGamesMessageBox(nullptr) { mState.viewing = NOTHING; mState.viewstyle = AUTOMATIC; @@ -84,6 +85,113 @@ ViewController::~ViewController() sInstance = nullptr; } +void ViewController::noSystemsFileDialog() +{ + std::string errorMessage = + "COULDN'T FIND THE SYSTEMS CONFIGURATION FILE.\n" + "ATTEMPTED TO COPY A TEMPLATE es_systems.cfg FILE\n" + "FROM THE EMULATIONSTATION RESOURCES DIRECTORY,\n" + "BUT THIS FAILED. HAS EMULATIONSTATION BEEN PROPERLY\n" + "INSTALLED AND DO YOU HAVE WRITE PERMISSIONS TO \n" + "YOUR HOME DIRECTORY?"; + + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + errorMessage.c_str(), + "QUIT", [] { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + }, "", nullptr, "", nullptr, true)); +} + +void ViewController::noGamesDialog() +{ + mNoGamesErrorMessage = + "THE SYSTEMS CONFIGURATION FILE EXISTS, BUT NO\n" + "GAME FILES WERE FOUND. EITHER PLACE YOUR GAMES\n" + "IN THE CURRENTLY CONFIGURED ROM DIRECTORY OR\n" + "CHANGE IT USING THE BUTTON BELOW. OPTIONALLY THE\n" + "ROM DIRECTORY STRUCTURE CAN BE GENERATED WHICH\n" + "WILL CREATE A TEXT FILE IN EACH FOLDER PROVIDING\n" + "SOME INFO SUCH AS THE SUPPORTED FILE EXTENSIONS.\n" + "THIS IS THE CURRENTLY CONFIGURED ROM DIRECTORY:\n"; + + #if defined(_WIN64) + mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); + #else + mRomDirectory = FileData::getROMDirectory(); + #endif + + mNoGamesMessageBox = new GuiMsgBox(mWindow, HelpStyle(), mNoGamesErrorMessage + mRomDirectory, + "CHANGE ROM DIRECTORY", [this] { + std::string currentROMDirectory; + #if defined(_WIN64) + currentROMDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); + #else + currentROMDirectory = FileData::getROMDirectory(); + #endif + + mWindow->pushGui(new GuiComplexTextEditPopup( + mWindow, + HelpStyle(), + "ENTER ROM DIRECTORY", + "Currently configured directory:", + currentROMDirectory, + currentROMDirectory, + [this](const std::string& newROMDirectory) { + Settings::getInstance()->setString("ROMDirectory", newROMDirectory); + Settings::getInstance()->saveFile(); + #if defined(_WIN64) + mRomDirectory = Utils::String::replace(FileData::getROMDirectory(), "/", "\\"); + #else + mRomDirectory = FileData::getROMDirectory(); + #endif + mNoGamesMessageBox->changeText(mNoGamesErrorMessage + mRomDirectory); + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + "ROM DIRECTORY SAVED, RESTART THE\n" + "APPLICATION TO RESCAN THE SYSTEMS", + "OK", nullptr, "", nullptr, "", nullptr, true)); + }, + false, + "SAVE", + "SAVE CHANGES?", + "LOAD CURRENT", + "LOAD CURRENTLY CONFIGURED VALUE", + "CLEAR", + "CLEAR (LEAVE BLANK TO RESET TO DEFAULT DIRECTORY)", + false)); + }, + "CREATE DIRECTORIES", [this] { + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + "THIS WILL CREATE DIRECTORIES FOR ALL THE\n" + "GAME SYSTEMS DEFINED IN es_systems.cfg\n\n" + "THIS MAY CREATE A LOT OF FOLDERS SO IT'S\n" + "ADVICED TO DELETE THE ONES YOU DON'T NEED\n\n" + "PROCEED?", + "YES", [this] { + if (!SystemData::createSystemDirectories()) { + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + "THE SYSTEM DIRECTORIES WERE SUCCESSFULLY CREATED ", "OK", nullptr, + "", nullptr, "", nullptr, true)); + } + else { + mWindow->pushGui(new GuiMsgBox(mWindow, HelpStyle(), + "ERROR CREATING THE SYSTEM DIRECTORIES,\n" + "PERMISSION PROBLEMS OR DISK FULL?\n\n" + "SEE THE LOG FILE FOR MORE DETAILS", "OK", nullptr, + "", nullptr, "", nullptr, true)); + } + }, "NO", nullptr, "", nullptr, true)); + }, + "QUIT", [] { + SDL_Event quit; + quit.type = SDL_QUIT; + SDL_PushEvent(&quit); + }, true, false); + + mWindow->pushGui(mNoGamesMessageBox); +} + void ViewController::goToStart() { // Check if the keyboard config is set as application default, meaning no user diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 4c10f8337..f8c50deda 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -4,14 +4,17 @@ // ViewController.h // // Handles overall system navigation including animations and transitions. -// Also creates the gamelist views and handles refresh and reloads of these when needed +// Creates the gamelist views and handles refresh and reloads of these when needed // (for example when metadata has been changed or when a list sorting has taken place). // Initiates the launching of games, calling FileData to do the actual launch. +// Displays a dialog when there are no games found on startup. // #ifndef ES_APP_VIEWS_VIEW_CONTROLLER_H #define ES_APP_VIEWS_VIEW_CONTROLLER_H +#include "guis/GuiComplexTextEditPopup.h" +#include "guis/GuiMsgBox.h" #include "renderers/Renderer.h" #include "FileData.h" #include "GuiComponent.h" @@ -32,6 +35,10 @@ public: virtual ~ViewController(); + // These functions are called from main(). + void noSystemsFileDialog(); + void noGamesDialog(); + // Try to completely populate the GameListView map. // Caches things so there's no pauses during transitions. void preload(); @@ -118,6 +125,10 @@ private: void launch(FileData* game); + std::string mNoGamesErrorMessage; + std::string mRomDirectory; + GuiMsgBox* mNoGamesMessageBox; + void playViewTransition(bool instant = false); int getSystemId(SystemData* system); // Restore view position if it was moved during wrap around.