Added support for selecting alternative emulators per game.

Also made some changes to the system-wide alternative emulators logic.
This commit is contained in:
Leon Styhre 2021-09-04 11:21:55 +02:00
parent 69ad5cc56f
commit 5942b2815e
13 changed files with 228 additions and 90 deletions

View file

@ -748,36 +748,41 @@ void FileData::launchGame(Window* window)
LOG(LogInfo) << "Launching game \"" << this->metadata.get("name") << "\"...";
std::string command = "";
std::string alternativeEmulator = getSystem()->getAlternativeEmulator();
// Check if there is a launch command override for the game
// and the corresponding option to use it has been set.
if (Settings::getInstance()->getBool("LaunchCommandOverride") &&
!metadata.get("launchcommand").empty()) {
command = metadata.get("launchcommand");
// Check if there is a game-specific alternative emulator configured.
// This takes precedence over any system-wide alternative emulator configuration.
if (Settings::getInstance()->getBool("AlternativeEmulatorPerGame") &&
!metadata.get("altemulator").empty()) {
command = getSystem()->getLaunchCommandFromLabel(metadata.get("altemulator"));
if (command == "") {
LOG(LogWarning) << "Invalid alternative emulator \"" << metadata.get("altemulator")
<< "\" configured for game";
}
else {
LOG(LogDebug) << "FileData::launchGame(): Using alternative emulator \""
<< metadata.get("altemulator") << "\" as configured for the game";
}
}
else {
std::string alternativeEmulator = getSystem()->getAlternativeEmulator();
for (auto launchCommand : mEnvData->mLaunchCommands) {
if (launchCommand.second == alternativeEmulator) {
command = launchCommand.first;
LOG(LogDebug) << "FileData::launchGame(): Using alternative emulator \""
<< alternativeEmulator << "\""
<< " for system \"" << this->getSystem()->getName() << "\"";
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;
// Check if there is a system-wide alternative emulator configured.
if (command == "" && alternativeEmulator != "") {
command = getSystem()->getLaunchCommandFromLabel(alternativeEmulator);
if (command == "") {
LOG(LogWarning) << "Invalid alternative emulator \""
<< alternativeEmulator.substr(9, alternativeEmulator.length() - 9)
<< "\" configured for system \"" << getSystem()->getName() << "\"";
}
else {
LOG(LogDebug) << "FileData::launchGame(): Using alternative emulator \""
<< getSystem()->getAlternativeEmulator() << "\""
<< " as configured for system \"" << this->getSystem()->getName() << "\"";
}
}
if (command.empty())
command = mEnvData->mLaunchCommands.front().first;
std::string commandRaw = command;
const std::string romPath = Utils::FileSystem::getEscapedPath(getPath());

View file

@ -132,7 +132,7 @@ void parseGamelist(SystemData* system)
<< "\" has a valid alternativeEmulator entry: \"" << label << "\"";
}
else {
system->setAlternativeEmulator("<INVALID>");
system->setAlternativeEmulator("<INVALID>" + label);
LOG(LogWarning) << "System \"" << system->getName()
<< "\" has an invalid alternativeEmulator entry that does "
"not match any command tag in es_systems.xml: \""

View file

@ -15,6 +15,8 @@
#include <pugixml.hpp>
// clang-format off
// The statistic entries must be placed at the bottom or otherwise there will be problems with
// saving the values in GuiMetaDataEd.
MetaDataDecl gameDecls[] = {
// key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd, shouldScrape
{"name", MD_STRING, "", false, "name", "enter name", true},
@ -34,9 +36,8 @@ MetaDataDecl gameDecls[] = {
{"nogamecount", MD_BOOL, "false", false, "exclude from game counter", "enter don't count as game off/on", false},
{"nomultiscrape", MD_BOOL, "false", false, "exclude from multi-scraper", "enter no multi-scrape off/on", false},
{"hidemetadata", MD_BOOL, "false", false, "hide metadata fields", "enter hide metadata off/on", false},
{"launchcommand", MD_LAUNCHCOMMAND, "", false, "launch command", "enter game launch command "
"(emulator override)", false},
{"playcount", MD_INT, "0", false, "times played", "enter number of times played", false},
{"altemulator", MD_ALT_EMULATOR, "", false, "alternative emulator", "select alternative emulator", false},
{"lastplayed", MD_TIME, "0", true, "last played", "enter last played date", false}
};

View file

@ -32,7 +32,7 @@ enum MetaDataType {
// Specialized types.
MD_MULTILINE_STRING,
MD_LAUNCHCOMMAND,
MD_ALT_EMULATOR,
MD_PATH,
MD_RATING,
MD_DATE,

View file

@ -501,10 +501,9 @@ bool SystemData::loadConfig()
}
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 << "\"";
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(
@ -615,6 +614,18 @@ bool SystemData::loadConfig()
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++)

View file

@ -99,6 +99,7 @@ public:
std::string getAlternativeEmulator() { return mAlternativeEmulator; }
void setAlternativeEmulator(const std::string& command) { mAlternativeEmulator = command; }
std::string getLaunchCommandFromLabel(const std::string& label);
static void deleteSystems();
// Loads the systems configuration file at getConfigPath() and creates the systems.

View file

@ -30,7 +30,7 @@ GuiAlternativeEmulators::GuiAlternativeEmulators(Window* window)
// Only include systems that have at least two command entries, unless the system
// has an invalid entry.
if ((*it)->getAlternativeEmulator() != "<INVALID>" &&
if ((*it)->getAlternativeEmulator().substr(0, 9) != "<INVALID>" &&
(*it)->getSystemEnvData()->mLaunchCommands.size() < 2)
continue;
@ -68,7 +68,7 @@ GuiAlternativeEmulators::GuiAlternativeEmulators(Window* window)
bool invalidEntry = false;
if (label.empty()) {
label = "<INVALID ENTRY>";
label = ViewController::EXCLAMATION_CHAR + " INVALID ENTRY";
invalidEntry = true;
}
@ -94,7 +94,11 @@ GuiAlternativeEmulators::GuiAlternativeEmulators(Window* window)
labelText->setSize(labelSizeX, labelText->getSize().y);
row.addElement(labelText, false);
row.makeAcceptInputHandler([this, it] { selectorWindow(*it); });
row.makeAcceptInputHandler([this, it, labelText] {
if (labelText->getValue() == "<REMOVED ENTRY>")
return;
selectorWindow(*it);
});
mMenu.addRow(row);
mHasSystems = true;
@ -104,9 +108,9 @@ GuiAlternativeEmulators::GuiAlternativeEmulators(Window* window)
// es_systems.xml.
if (!mHasSystems) {
ComponentListRow row;
std::shared_ptr<TextComponent> systemText =
std::make_shared<TextComponent>(mWindow, "<NO ALTERNATIVE EMULATORS DEFINED>",
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
std::shared_ptr<TextComponent> systemText = std::make_shared<TextComponent>(
mWindow, ViewController::EXCLAMATION_CHAR + " NO ALTERNATIVE EMULATORS DEFINED",
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
row.addElement(systemText, true);
mMenu.addRow(row);
}
@ -150,13 +154,16 @@ void GuiAlternativeEmulators::selectorWindow(SystemData* system)
ComponentListRow row;
if (entry.second == "")
label = "<REMOVE INVALID ENTRY>";
label = "<CLEAR INVALID ENTRY>";
else
label = entry.second;
std::shared_ptr<TextComponent> labelText = std::make_shared<TextComponent>(
mWindow, label, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
if (system->getSystemEnvData()->mLaunchCommands.front().second == label)
labelText->setValue(labelText->getValue().append(" [DEFAULT]"));
row.addElement(labelText, true);
row.makeAcceptInputHandler([this, s, system, labelText, entry, selectedLabel] {
if (entry.second != selectedLabel) {
@ -165,9 +172,25 @@ void GuiAlternativeEmulators::selectorWindow(SystemData* system)
else
system->setAlternativeEmulator(entry.second);
updateGamelist(system, true);
updateMenu(
system->getName(), labelText->getValue(),
(entry.second == system->getSystemEnvData()->mLaunchCommands.front().second));
if (entry.second == system->getSystemEnvData()->mLaunchCommands.front().second) {
if (system->getSystemEnvData()->mLaunchCommands.front().second == "") {
updateMenu(system->getName(), "<REMOVED ENTRY>",
(entry.second ==
system->getSystemEnvData()->mLaunchCommands.front().second));
}
else {
updateMenu(system->getName(),
system->getSystemEnvData()->mLaunchCommands.front().second,
(entry.second ==
system->getSystemEnvData()->mLaunchCommands.front().second));
}
}
else {
updateMenu(system->getName(), labelText->getValue(),
(entry.second ==
system->getSystemEnvData()->mLaunchCommands.front().second));
}
}
delete s;
});

View file

@ -1021,16 +1021,17 @@ void GuiMenu::openOtherOptions()
}
});
// Allow overriding of the launch command per game (the option to disable this is
// intended primarily for testing purposes).
auto launchcommand_override = std::make_shared<SwitchComponent>(mWindow);
launchcommand_override->setState(Settings::getInstance()->getBool("LaunchCommandOverride"));
s->addWithLabel("PER GAME LAUNCH COMMAND OVERRIDE", launchcommand_override);
s->addSaveFunc([launchcommand_override, s] {
if (launchcommand_override->getState() !=
Settings::getInstance()->getBool("LaunchCommandOverride")) {
Settings::getInstance()->setBool("LaunchCommandOverride",
launchcommand_override->getState());
// Whether to enable alternative emulators per game (the option to disable this is intended
// primarily for testing purposes).
auto alternativeEmulatorPerGame = std::make_shared<SwitchComponent>(mWindow);
alternativeEmulatorPerGame->setState(
Settings::getInstance()->getBool("AlternativeEmulatorPerGame"));
s->addWithLabel("ENABLE ALTERNATIVE EMULATORS PER GAME", alternativeEmulatorPerGame);
s->addSaveFunc([alternativeEmulatorPerGame, s] {
if (alternativeEmulatorPerGame->getState() !=
Settings::getInstance()->getBool("AlternativeEmulatorPerGame")) {
Settings::getInstance()->setBool("AlternativeEmulatorPerGame",
alternativeEmulatorPerGame->getState());
s->setNeedsSaving();
}
});

View file

@ -49,6 +49,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window,
, mClearGameFunc(clearGameFunc)
, mDeleteGameFunc(deleteGameFunc)
, mMediaFilesUpdated(false)
, mInvalidEmulatorEntry(false)
{
addChild(&mBackground);
addChild(&mGrid);
@ -99,9 +100,9 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window,
if (iter->isStatistic)
continue;
// Don't show the launch command override entry if this option has been disabled.
if (!Settings::getInstance()->getBool("LaunchCommandOverride") &&
iter->type == MD_LAUNCHCOMMAND) {
// Don't show the alternative emulator entry if the corresponding option has been disabled.
if (!Settings::getInstance()->getBool("AlternativeEmulatorPerGame") &&
iter->type == MD_ALT_EMULATOR) {
ed = std::make_shared<TextComponent>(
window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
assert(ed);
@ -170,7 +171,9 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window,
std::placeholders::_2);
break;
}
case MD_LAUNCHCOMMAND: {
case MD_ALT_EMULATOR: {
mInvalidEmulatorEntry = false;
ed = std::make_shared<TextComponent>(window, "",
Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
0x777777FF, ALIGN_RIGHT);
@ -189,44 +192,124 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window,
const std::string title = iter->displayPrompt;
// OK callback (apply new value to ed).
auto updateVal = [ed, originalValue](const std::string& newVal) {
auto updateVal = [this, ed, originalValue](const std::string& newVal) {
ed->setValue(newVal);
if (newVal == originalValue)
if (newVal == originalValue) {
ed->setColor(DEFAULT_TEXTCOLOR);
else
}
else {
ed->setColor(TEXTCOLOR_USERMARKED);
mInvalidEmulatorEntry = false;
}
};
std::string staticTextString = "Default value from es_systems.xml:";
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()) {
if (originalValue != "" &&
scraperParams.system->getLaunchCommandFromLabel(originalValue) == "") {
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
<< "\"";
<< "GuiMetaDataEd: Invalid alternative emulator \"" << originalValue
<< "\" configured for game \"" << mScraperParams.game->getName() << "\"";
mInvalidEmulatorEntry = true;
}
if (defaultLaunchCommand.empty())
defaultLaunchCommand =
scraperParams.system->getSystemEnvData()->mLaunchCommands.front().first;
if (scraperParams.system->getSystemEnvData()->mLaunchCommands.size() == 1) {
lbl->setOpacity(DISABLED_OPACITY);
bracket->setOpacity(DISABLED_OPACITY);
}
row.makeAcceptInputHandler([this, title, staticTextString, defaultLaunchCommand, ed,
updateVal, multiLine] {
mWindow->pushGui(new GuiComplexTextEditPopup(
mWindow, getHelpStyle(), title, staticTextString, defaultLaunchCommand,
ed->getValue(), updateVal, multiLine, "APPLY", "APPLY CHANGES?"));
});
if (mInvalidEmulatorEntry ||
scraperParams.system->getSystemEnvData()->mLaunchCommands.size() > 1) {
row.makeAcceptInputHandler([this, title, scraperParams, ed, updateVal] {
auto s = new GuiSettings(mWindow, title);
if (!mInvalidEmulatorEntry && ed->getValue() == "" &&
scraperParams.system->getSystemEnvData()->mLaunchCommands.size() == 1)
return;
std::vector<std::pair<std::string, std::string>> launchCommands =
scraperParams.system->getSystemEnvData()->mLaunchCommands;
if (ed->getValue() != "" && mInvalidEmulatorEntry)
launchCommands.push_back(std::make_pair("", "<CLEAR INVALID ENTRY>"));
else if (ed->getValue() != "")
launchCommands.push_back(std::make_pair("", "<CLEAR ENTRY>"));
for (auto entry : launchCommands) {
std::string selectedLabel = ed->getValue();
std::string label;
ComponentListRow row;
if (entry.second == "")
continue;
else
label = entry.second;
std::shared_ptr<TextComponent> labelText =
std::make_shared<TextComponent>(mWindow, label,
Font::get(FONT_SIZE_MEDIUM),
0x777777FF, ALIGN_CENTER);
if (scraperParams.system->getAlternativeEmulator() == "" &&
scraperParams.system->getSystemEnvData()
->mLaunchCommands.front()
.second == label)
labelText->setValue(labelText->getValue().append(" [SYSTEM-WIDE]"));
if (scraperParams.system->getAlternativeEmulator() == label)
labelText->setValue(labelText->getValue().append(" [SYSTEM-WIDE]"));
row.addElement(labelText, true);
row.makeAcceptInputHandler(
[this, s, updateVal, entry, selectedLabel, launchCommands] {
if (entry.second == launchCommands.back().second &&
launchCommands.back().first == "") {
updateVal("");
}
else if (entry.second != selectedLabel) {
updateVal(entry.second);
}
mInvalidEmulatorEntry = false;
delete s;
});
// This transparent bracket is only added to generate the correct help
// prompts.
auto bracket = std::make_shared<ImageComponent>(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<float>(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);
});
}
else {
lbl->setOpacity(DISABLED_OPACITY);
bracket->setOpacity(DISABLED_OPACITY);
}
break;
}
case MD_MULTILINE_STRING:
@ -292,7 +375,12 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window,
assert(ed);
mList->addRow(row);
ed->setValue(mMetaData->get(iter->key));
if (iter->type == MD_ALT_EMULATOR && mInvalidEmulatorEntry == true)
ed->setValue(ViewController::EXCLAMATION_CHAR + " INVALID ENTRY ");
else
ed->setValue(mMetaData->get(iter->key));
mEditors.push_back(ed);
}
@ -435,6 +523,9 @@ void GuiMetaDataEd::save()
if (mMetaDataDecl.at(i).isStatistic)
continue;
if (mMetaDataDecl.at(i).key == "altemulator" && mInvalidEmulatorEntry == true)
continue;
if (!showHiddenGames && mMetaDataDecl.at(i).key == "hidden" &&
mEditors.at(i)->getValue() != mMetaData->get("hidden"))
hideGameWhileHidden = true;
@ -576,6 +667,9 @@ void GuiMetaDataEd::close()
std::string mMetaDataValue = mMetaData->get(key);
std::string mEditorsValue = mEditors.at(i)->getValue();
if (key == "altemulator" && mInvalidEmulatorEntry == true)
continue;
if (mMetaDataValue != mEditorsValue) {
metadataUpdated = true;
break;

View file

@ -15,6 +15,7 @@
#include "MetaData.h"
#include "components/ComponentGrid.h"
#include "components/NinePatchComponent.h"
#include "guis/GuiSettings.h"
#include "scrapers/Scraper.h"
class ComponentList;
@ -64,6 +65,7 @@ private:
std::function<void()> mDeleteGameFunc;
bool mMediaFilesUpdated;
bool mInvalidEmulatorEntry;
};
#endif // ES_APP_GUIS_GUI_META_DATA_ED_H

View file

@ -617,7 +617,7 @@ int main(int argc, char* argv[])
// 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() == "<INVALID>") {
if (system->getAlternativeEmulator().substr(0, 9) == "<INVALID>") {
ViewController::get()->invalidAlternativeEmulatorDialog();
break;
}

View file

@ -205,7 +205,7 @@ void ViewController::invalidAlternativeEmulatorDialog()
"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"));
"INTERFACE IN THE 'OTHER SETTINGS' MENU"));
}
void ViewController::goToStart()

View file

@ -240,7 +240,7 @@ void Settings::setDefaults()
mBoolMap["VideoHardwareDecoding"] = {false, false};
#endif
mBoolMap["VideoUpscaleFrameRate"] = {false, false};
mBoolMap["LaunchCommandOverride"] = {true, true};
mBoolMap["AlternativeEmulatorPerGame"] = {true, true};
mBoolMap["ShowHiddenFiles"] = {true, true};
mBoolMap["ShowHiddenGames"] = {true, true};
mBoolMap["CustomEventScripts"] = {false, false};