diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 9af8aaee8..7511c4835 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -16,6 +16,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h # GUIs + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.h @@ -66,6 +67,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp # GUIs + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiAlternativeEmulators.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.cpp diff --git a/es-app/src/CollectionSystemsManager.cpp b/es-app/src/CollectionSystemsManager.cpp index 60c4846fd..6685db3ca 100644 --- a/es-app/src/CollectionSystemsManager.cpp +++ b/es-app/src/CollectionSystemsManager.cpp @@ -73,7 +73,8 @@ CollectionSystemsManager::CollectionSystemsManager(Window* window) mCollectionEnvData->mStartPath = ""; std::vector exts; mCollectionEnvData->mSearchExtensions = exts; - mCollectionEnvData->mLaunchCommand = ""; + std::vector> commands; + mCollectionEnvData->mLaunchCommands = commands; std::vector allPlatformIds; allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE); mCollectionEnvData->mPlatformIds = allPlatformIds; diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 9ca4b0a8d..b0e16298f 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -756,7 +756,22 @@ void FileData::launchGame(Window* window) command = metadata.get("launchcommand"); } else { - command = mEnvData->mLaunchCommand; + std::string alternativeEmulator = getSystem()->getAlternativeEmulator(); + for (auto launchCommand : mEnvData->mLaunchCommands) { + if (launchCommand.second == alternativeEmulator) { + command = launchCommand.first; + break; + } + } + if (!alternativeEmulator.empty() && command.empty()) { + LOG(LogWarning) << "The alternative emulator configured for system \"" + << getSystem()->getName() + << "\" is invalid, falling back to the default command \"" + << getSystem()->getSystemEnvData()->mLaunchCommands.front().first << "\""; + } + + if (command.empty()) + command = mEnvData->mLaunchCommands.front().first; } std::string commandRaw = command; diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index c541db582..ac71f8d80 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -117,6 +117,30 @@ void parseGamelist(SystemData* system) return; } + pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); + if (alternativeEmulator) { + std::string label = alternativeEmulator.child("label").text().get(); + if (label != "") { + bool validLabel = false; + for (auto command : system->getSystemEnvData()->mLaunchCommands) { + if (command.second == label) + validLabel = true; + } + if (validLabel) { + system->setAlternativeEmulator(label); + LOG(LogDebug) << "Gamelist::parseGamelist(): System \"" << system->getName() + << "\" has a valid alternativeEmulator entry: \"" << label << "\""; + } + else { + system->setAlternativeEmulator(""); + LOG(LogWarning) << "System \"" << system->getName() + << "\" has an invalid alternativeEmulator entry that does " + "not match any command tag in es_systems.xml: \"" + << label << "\""; + } + } + } + std::string relativeTo = system->getStartPath(); bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); @@ -221,7 +245,7 @@ void addFileDataNode(pugi::xml_node& parent, } } -void updateGamelist(SystemData* system) +void updateGamelist(SystemData* system, bool updateAlternativeEmulator) { // We do this by reading the XML again, adding changes and then writing them back, // because there might be information missing in our systemdata which we would otherwise @@ -256,8 +280,39 @@ void updateGamelist(SystemData* system) LOG(LogError) << "Couldn't find node in gamelist \"" << xmlReadPath << "\""; return; } + if (updateAlternativeEmulator) { + pugi::xml_node alternativeEmulator = doc.child("alternativeEmulator"); + + if (system->getAlternativeEmulator() != "") { + if (!alternativeEmulator) { + doc.prepend_child("alternativeEmulator"); + alternativeEmulator = doc.child("alternativeEmulator"); + } + + pugi::xml_node label = alternativeEmulator.child("label"); + + if (label && system->getAlternativeEmulator() != + alternativeEmulator.child("label").text().get()) { + alternativeEmulator.remove_child(label); + alternativeEmulator.prepend_child("label").text().set( + system->getAlternativeEmulator().c_str()); + } + else if (!label) { + alternativeEmulator.prepend_child("label").text().set( + system->getAlternativeEmulator().c_str()); + } + } + else if (alternativeEmulator) { + doc.remove_child("alternativeEmulator"); + } + } } else { + if (updateAlternativeEmulator && system->getAlternativeEmulator() != "") { + pugi::xml_node alternativeEmulator = doc.prepend_child("alternativeEmulator"); + alternativeEmulator.prepend_child("label").text().set( + system->getAlternativeEmulator().c_str()); + } // Set up an empty gamelist to append to. root = doc.append_child("gameList"); } @@ -312,15 +367,31 @@ void updateGamelist(SystemData* system) } // Now write the file. - if (numUpdated > 0) { + if (numUpdated > 0 || updateAlternativeEmulator) { // Make sure the folders leading up to this path exist (or the write will fail). std::string xmlWritePath(system->getGamelistPath(true)); Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); - LOG(LogDebug) << "Gamelist::updateGamelist(): Added/updated " << numUpdated - << (numUpdated == 1 ? " entity in \"" : " entities in \"") << xmlReadPath - << "\""; - + if (updateAlternativeEmulator) { + if (system->getAlternativeEmulator() == "") { + LOG(LogDebug) << "Gamelist::updateGamelist(): Removed the " + "alternativeEmulator tag for system \"" + << system->getName() << "\" as the default emulator \"" + << system->getSystemEnvData()->mLaunchCommands.front().second + << "\" was selected"; + } + else { + LOG(LogDebug) << "Gamelist::updateGamelist(): " + "Added/updated the alternativeEmulator tag for system \"" + << system->getName() << "\" to \"" + << system->getAlternativeEmulator() << "\""; + } + } + if (numUpdated > 0) { + LOG(LogDebug) << "Gamelist::updateGamelist(): Added/updated " << numUpdated + << (numUpdated == 1 ? " entity in \"" : " entities in \"") + << xmlWritePath << "\""; + } #if defined(_WIN64) if (!doc.save_file(Utils::String::stringToWideString(xmlWritePath).c_str())) { #else diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index a8f91a027..46c8dd10c 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -15,6 +15,6 @@ class SystemData; void parseGamelist(SystemData* system); // Writes currently loaded metadata for a SystemData to gamelist.xml. -void updateGamelist(SystemData* system); +void updateGamelist(SystemData* system, bool updateAlternativeEmulator = false); #endif // ES_APP_GAME_LIST_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 643cd1923..630db6fb8 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -417,13 +417,27 @@ bool SystemData::loadConfig() std::string name; std::string fullname; std::string path; - std::string cmd; std::string themeFolder; name = system.child("name").text().get(); fullname = system.child("fullname").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 "" @@ -461,7 +475,41 @@ bool SystemData::loadConfig() // Convert extensions list from a string into a vector of strings. std::vector extensions = readList(system.child("extension").text().get()); - cmd = system.child("command").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> 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 default 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 default 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 = @@ -504,7 +552,7 @@ bool SystemData::loadConfig() << "A system in the es_systems.xml file has no name defined, skipping entry"; continue; } - else if (fullname.empty() || path.empty() || extensions.empty() || cmd.empty()) { + 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"; @@ -526,7 +574,7 @@ bool SystemData::loadConfig() SystemEnvironmentData* envData = new SystemEnvironmentData; envData->mStartPath = path; envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; + envData->mLaunchCommands = commands; envData->mPlatformIds = platformIds; SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); @@ -679,7 +727,7 @@ bool SystemData::createSystemDirectories() std::string fullname; std::string path; std::string extensions; - std::string command; + std::vector commands; std::string platform; std::string themeFolder; const std::string systemInfoFileName = "/systeminfo.txt"; @@ -690,7 +738,10 @@ bool SystemData::createSystemDirectories() fullname = system.child("fullname").text().get(); path = system.child("path").text().get(); extensions = system.child("extension").text().get(); - command = system.child("command").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()); @@ -757,7 +808,16 @@ bool SystemData::createSystemDirectories() systemInfoFile << "Supported file extensions:" << std::endl; systemInfoFile << extensions << std::endl << std::endl; systemInfoFile << "Launch command:" << std::endl; - systemInfoFile << command << std::endl << 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; diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 28a44721a..5927c2385 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -27,7 +27,7 @@ class ThemeData; struct SystemEnvironmentData { std::string mStartPath; std::vector mSearchExtensions; - std::string mLaunchCommand; + std::vector> mLaunchCommands; std::vector mPlatformIds; }; @@ -97,6 +97,9 @@ public: bool getScrapeFlag() { return mScrapeFlag; } void setScrapeFlag(bool scrapeflag) { mScrapeFlag = scrapeflag; } + std::string getAlternativeEmulator() { return mAlternativeEmulator; } + void setAlternativeEmulator(const std::string& command) { mAlternativeEmulator = command; } + static void deleteSystems(); // Loads the systems configuration file at getConfigPath() and creates the systems. static bool loadConfig(); @@ -153,6 +156,7 @@ private: std::string mName; std::string mFullName; SystemEnvironmentData* mEnvData; + std::string mAlternativeEmulator; std::string mThemeFolder; std::shared_ptr mTheme; diff --git a/es-app/src/guis/GuiAlternativeEmulators.cpp b/es-app/src/guis/GuiAlternativeEmulators.cpp new file mode 100644 index 000000000..ca5bb888e --- /dev/null +++ b/es-app/src/guis/GuiAlternativeEmulators.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiAlternativeEmulators.cpp +// +// User interface to select between alternative emulators per system +// based on configuration entries in es_systems.xml. +// + +#include "guis/GuiAlternativeEmulators.h" + +#include "Gamelist.h" +#include "SystemData.h" +#include "views/ViewController.h" + +GuiAlternativeEmulators::GuiAlternativeEmulators(Window* window) + : GuiComponent{window} + , mMenu{window, "ALTERNATIVE EMULATORS"} + , mHasSystems(false) +{ + addChild(&mMenu); + mMenu.addButton("BACK", "back", [this] { delete this; }); + + // Horizontal sizes for the system and label entries. + float systemSizeX = mMenu.getSize().x / 3.27f; + float labelSizeX = mMenu.getSize().x / 1.53; + + for (auto it = SystemData::sSystemVector.cbegin(); // Line break. + it != SystemData::sSystemVector.cend(); it++) { + + // Only include systems that have at least two command entries, unless the system + // has an invalid entry. + if ((*it)->getAlternativeEmulator() != "" && + (*it)->getSystemEnvData()->mLaunchCommands.size() < 2) + continue; + + ComponentListRow row; + + // This transparent bracket is only added to generate a left margin. + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/graphics/arrow.svg"); + bracket->setOpacity(0); + bracket->setSize(bracket->getSize() / 3.0f); + row.addElement(bracket, false); + + std::string name = (*it)->getName(); + std::shared_ptr systemText = + std::make_shared(mWindow, name, Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + + systemText->setSize(systemSizeX, systemText->getSize().y); + row.addElement(systemText, false); + + std::string configuredLabel = (*it)->getAlternativeEmulator(); + std::string label; + + if (configuredLabel == "") { + label = (*it)->getSystemEnvData()->mLaunchCommands.front().second; + } + else { + for (auto command : (*it)->getSystemEnvData()->mLaunchCommands) { + if (command.second == configuredLabel) { + label = command.second; + break; + } + } + } + + bool invalidEntry = false; + + if (label.empty()) { + label = ""; + invalidEntry = true; + } + + std::shared_ptr labelText; + + if (label == (*it)->getSystemEnvData()->mLaunchCommands.front().second) { + labelText = std::make_shared( + mWindow, label, Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), 0x777777FF, + ALIGN_RIGHT); + } + else { + // Mark any non-default value with bold and add a gear symbol as well. + labelText = std::make_shared( + mWindow, label + (!invalidEntry ? " " + ViewController::GEAR_CHAR : ""), + Font::get(FONT_SIZE_MEDIUM, FONT_PATH_BOLD), 0x777777FF, ALIGN_RIGHT); + } + + // Mark invalid entries with red color. + if (invalidEntry) + labelText->setColor(TEXTCOLOR_SCRAPERMARKED); + + mCommandRows[name] = labelText; + labelText->setSize(labelSizeX, labelText->getSize().y); + + row.addElement(labelText, false); + row.makeAcceptInputHandler([this, it] { selectorWindow(*it); }); + + mMenu.addRow(row); + mHasSystems = true; + } + + // Add a dummy row if no enabled systems have any alternative emulators defined in + // es_systems.xml. + if (!mHasSystems) { + ComponentListRow row; + std::shared_ptr systemText = + std::make_shared(mWindow, "", + Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + row.addElement(systemText, true); + mMenu.addRow(row); + } + + float width = + static_cast(std::min(static_cast(Renderer::getScreenHeight() * 1.05f), + static_cast(Renderer::getScreenWidth() * 0.90f))); + + setSize(mMenu.getSize()); + setPosition((Renderer::getScreenWidth() - mSize.x) / 2.0f, Renderer::getScreenHeight() * 0.13f); + + mMenu.setSize(width, Renderer::getScreenHeight() * 0.76f); +} + +void GuiAlternativeEmulators::updateMenu(const std::string& systemName, + const std::string& label, + bool defaultEmulator) +{ + // If another label was selected, then update the menu accordingly. + if (defaultEmulator) { + mCommandRows[systemName].get()->setFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT)); + mCommandRows[systemName].get()->setValue(label); + } + else { + // Mark any non-default value with bold and add a gear symbol as well. + mCommandRows[systemName].get()->setFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_BOLD)); + mCommandRows[systemName].get()->setValue(label + " " + ViewController::GEAR_CHAR); + } + + mCommandRows[systemName].get()->setColor(DEFAULT_TEXTCOLOR); +} + +void GuiAlternativeEmulators::selectorWindow(SystemData* system) +{ + auto s = new GuiSettings(mWindow, system->getFullName()); + + std::string selectedLabel = system->getAlternativeEmulator(); + std::string label; + + for (auto entry : system->getSystemEnvData()->mLaunchCommands) { + ComponentListRow row; + + if (entry.second == "") + label = ""; + else + label = entry.second; + + std::shared_ptr labelText = std::make_shared( + mWindow, label, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + + row.addElement(labelText, true); + row.makeAcceptInputHandler([this, s, system, labelText, entry, selectedLabel] { + if (entry.second != selectedLabel) { + if (entry.second == system->getSystemEnvData()->mLaunchCommands.front().second) + system->setAlternativeEmulator(""); + else + system->setAlternativeEmulator(entry.second); + updateGamelist(system, true); + updateMenu( + system->getName(), labelText->getValue(), + (entry.second == system->getSystemEnvData()->mLaunchCommands.front().second)); + } + delete s; + }); + + // This transparent bracket is only added to generate the correct help prompts. + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/graphics/arrow.svg"); + bracket->setOpacity(0); + bracket->setSize(bracket->getSize() / 3.0f); + row.addElement(bracket, false); + + // Select the row that corresponds to the selected label. + if (selectedLabel == label) + s->addRow(row, true); + else + s->addRow(row, false); + } + + // Adjust the width depending on the aspect ratio of the screen, to make the screen look + // somewhat coherent regardless of screen type. The 1.778 aspect ratio value is the 16:9 + // reference. + float aspectValue = 1.778f / Renderer::getScreenAspectRatio(); + + float maxWidthModifier = glm::clamp(0.70f * aspectValue, 0.50f, 0.92f); + float maxWidth = static_cast(Renderer::getScreenWidth()) * maxWidthModifier; + + s->setMenuSize(glm::vec2{maxWidth, s->getMenuSize().y}); + + auto menuSize = s->getMenuSize(); + auto menuPos = s->getMenuPosition(); + + s->setMenuPosition(glm::vec3{(s->getSize().x - menuSize.x) / 2.0f, + (s->getSize().y - menuSize.y) / 3.0f, menuPos.z}); + mWindow->pushGui(s); +} + +bool GuiAlternativeEmulators::input(InputConfig* config, Input input) +{ + if (input.value != 0 && (config->isMappedTo("b", input) || config->isMappedTo("back", input))) { + delete this; + return true; + } + + return mMenu.input(config, input); +} + +std::vector GuiAlternativeEmulators::getHelpPrompts() +{ + std::vector prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", "back")); + if (mHasSystems) + prompts.push_back(HelpPrompt("a", "select")); + return prompts; +} + +HelpStyle GuiAlternativeEmulators::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} diff --git a/es-app/src/guis/GuiAlternativeEmulators.h b/es-app/src/guis/GuiAlternativeEmulators.h new file mode 100644 index 000000000..f243939e1 --- /dev/null +++ b/es-app/src/guis/GuiAlternativeEmulators.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GuiAlternativeEmulators.h +// +// User interface to select between alternative emulators per system +// based on configuration entries in es_systems.xml. +// + +#ifndef ES_APP_GUIS_GUI_ALTERNATIVE_EMULATORS_H +#define ES_APP_GUIS_GUI_ALTERNATIVE_EMULATORS_H + +#include "GuiComponent.h" +#include "guis/GuiSettings.h" + +template class OptionListComponent; + +class GuiAlternativeEmulators : public GuiComponent +{ +public: + GuiAlternativeEmulators(Window* window); + +private: + void updateMenu(const std::string& systemName, const std::string& label, bool defaultEmulator); + void selectorWindow(SystemData* system); + + virtual bool input(InputConfig* config, Input input) override; + virtual std::vector getHelpPrompts() override; + HelpStyle getHelpStyle() override; + + MenuComponent mMenu; + bool mHasSystems; + + std::map> mCommandRows; + std::shared_ptr> mCommandSelection; +}; + +#endif // ES_APP_GUIS_GUI_ALTERNATIVE_EMULATORS_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 4e8421192..e77910616 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -20,6 +20,7 @@ #include "components/OptionListComponent.h" #include "components/SliderComponent.h" #include "components/SwitchComponent.h" +#include "guis/GuiAlternativeEmulators.h" #include "guis/GuiCollectionSystemsOptions.h" #include "guis/GuiComplexTextEditPopup.h" #include "guis/GuiDetectDevice.h" @@ -772,6 +773,18 @@ void GuiMenu::openOtherOptions() { auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); + // Alternative emulators GUI. + ComponentListRow alternativeEmulatorsRow; + alternativeEmulatorsRow.elements.clear(); + alternativeEmulatorsRow.addElement( + std::make_shared(mWindow, "ALTERNATIVE EMULATORS", + Font::get(FONT_SIZE_MEDIUM), 0x777777FF), + true); + alternativeEmulatorsRow.addElement(makeArrow(mWindow), false); + alternativeEmulatorsRow.makeAcceptInputHandler( + std::bind([this] { mWindow->pushGui(new GuiAlternativeEmulators(mWindow)); })); + s->addRow(alternativeEmulatorsRow); + // Game media directory. ComponentListRow rowMediaDir; auto media_directory = std::make_shared(mWindow, "GAME MEDIA DIRECTORY", diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 31cabc75b..3ce434094 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -198,8 +198,28 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, }; std::string staticTextString = "Default value from es_systems.xml:"; - std::string defaultLaunchCommand = - scraperParams.system->getSystemEnvData()->mLaunchCommand; + std::string defaultLaunchCommand; + + std::string alternativeEmulator = scraperParams.system->getAlternativeEmulator(); + for (auto launchCommand : + scraperParams.system->getSystemEnvData()->mLaunchCommands) { + if (launchCommand.second == alternativeEmulator) { + defaultLaunchCommand = launchCommand.first; + break; + } + } + if (!alternativeEmulator.empty() && defaultLaunchCommand.empty()) { + LOG(LogWarning) + << "The alternative emulator defined for system \"" + << scraperParams.system->getName() + << "\" is invalid, falling back to the default command \"" + << scraperParams.system->getSystemEnvData()->mLaunchCommands.front().first + << "\""; + } + + if (defaultLaunchCommand.empty()) + defaultLaunchCommand = + scraperParams.system->getSystemEnvData()->mLaunchCommands.front().first; row.makeAcceptInputHandler([this, title, staticTextString, defaultLaunchCommand, ed, updateVal, multiLine] { diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 97a0fa7ad..d597706f9 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -613,6 +613,16 @@ int main(int argc, char* argv[]) } } + // Check if any of the enabled systems has an invalid alternative emulator entry, + // which means that a label is present in the gamelist.xml file which is not matching + // any command tag in es_systems.xml. + for (auto system : SystemData::sSystemVector) { + if (system->getAlternativeEmulator() == "") { + ViewController::get()->invalidAlternativeEmulatorDialog(); + break; + } + } + // Don't generate controller events while we're loading. SDL_GameControllerEventState(SDL_DISABLE); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 11640dac8..577554d3c 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -41,12 +41,14 @@ const std::string ViewController::FOLDER_CHAR = Utils::String::wideStringToStrin const std::string ViewController::TICKMARK_CHAR = Utils::String::wideStringToString(L"\uF14A"); const std::string ViewController::CONTROLLER_CHAR = Utils::String::wideStringToString(L"\uF11b"); const std::string ViewController::FILTER_CHAR = Utils::String::wideStringToString(L"\uF0b0"); +const std::string ViewController::GEAR_CHAR = Utils::String::wideStringToString(L"\uF013"); #else const std::string ViewController::FAVORITE_CHAR = "\uF005"; const std::string ViewController::FOLDER_CHAR = "\uF07C"; const std::string ViewController::TICKMARK_CHAR = "\uF14A"; const std::string ViewController::CONTROLLER_CHAR = "\uF11b"; const std::string ViewController::FILTER_CHAR = "\uF0b0"; +const std::string ViewController::GEAR_CHAR = "\uF013"; #endif ViewController* ViewController::get() @@ -193,6 +195,17 @@ void ViewController::noGamesDialog() mWindow->pushGui(mNoGamesMessageBox); } +void ViewController::invalidAlternativeEmulatorDialog() +{ + mWindow->pushGui(new GuiMsgBox(mWindow, getHelpStyle(), + "AT LEAST ONE OF YOUR SYSTEMS HAS AN\n" + "INVALID ALTERNATIVE EMULATOR CONFIGURED\n" + "WITH NO MATCHING ENTRY IN THE SYSTEMS\n" + "CONFIGURATION FILE, PLEASE REVIEW YOUR\n" + "SETUP USING THE 'ALTERNATIVE EMULATORS'\n" + "ENTRY UNDER THE 'OTHER SETTINGS' MENU")); +} + void ViewController::goToStart() { // If the system view does not exist, then create it. We do this here as it would diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index ee1bcc211..243558947 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -38,6 +38,7 @@ public: // These functions are called from main(). void invalidSystemsFileDialog(); void noGamesDialog(); + void invalidAlternativeEmulatorDialog(); // Try to completely populate the GameListView map. // Caches things so there's no pauses during transitions. @@ -127,6 +128,7 @@ public: static const std::string TICKMARK_CHAR; static const std::string CONTROLLER_CHAR; static const std::string FILTER_CHAR; + static const std::string GEAR_CHAR; private: ViewController(Window* window); diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 4ce1c52cf..fc21ce2f5 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -223,7 +223,11 @@ private: HelpStyle mHelpStyle; - void open() { mWindow->pushGui(new OptionListPopup(mWindow, getHelpStyle(), this, mName)); } + void open() + { + // Open the list popup. + mWindow->pushGui(new OptionListPopup(mWindow, getHelpStyle(), this, mName)); + } void onSelectedChanged() {