#include "Cafe/GameProfile/GameProfile.h" #include "util/helpers/helpers.h" #include "boost/nowide/convert.hpp" #include "config/ActiveSettings.h" #include "Common/FileStream.h" #include "util/IniParser/IniParser.h" #include "util/helpers/StringHelpers.h" #include "Cafe/CafeSystem.h" std::unique_ptr g_current_game_profile = std::make_unique(); struct gameProfileBooleanOption_t { bool isPresent = false; bool value; }; /* * Attempts to load a boolean option * If the option exists, true is returned. * The boolean is stored in *optionValue */ bool gameProfile_loadBooleanOption(IniParser* iniParser, char* optionName, gameProfileBooleanOption_t* option) { auto option_value = iniParser->FindOption(optionName); option->isPresent = false; option->value = false; if (!option_value) return false; // parse option if (boost::iequals(*option_value, "false") || boost::iequals(*option_value, "0")) { option->isPresent = true; option->value = false; return true; } else if (boost::iequals(*option_value, "true") || boost::iequals(*option_value, "1")) { option->isPresent = true; option->value = true; return true; } else cemuLog_log(LogType::Force, "Unknown value '{}' for option '{}' in game profile", *option_value, optionName); return false; } bool gameProfile_loadBooleanOption2(IniParser& iniParser, const char* optionName, bool& option) { auto option_value = iniParser.FindOption(optionName); if (!option_value) return false; if (boost::iequals(*option_value, "1") || boost::iequals(*option_value, "true")) { option = true; return true; } else if (boost::iequals(*option_value, "0") || boost::iequals(*option_value, "false")) { option = false; return true; } else cemuLog_log(LogType::Force, "Unknown value '{}' for option '{}' in game profile", *option_value, optionName); return false; } bool gameProfile_loadBooleanOption2(IniParser& iniParser, const char* optionName, std::optional& option) { bool tmp; const auto result = gameProfile_loadBooleanOption2(iniParser, optionName, tmp); if(result) option = tmp; return result; } /* * Attempts to load a integer option * Allows to specify min and max value (error is logged if out of range and default value is picked) */ bool gameProfile_loadIntegerOption(IniParser* iniParser, const char* optionName, gameProfileIntegerOption_t* option, sint32 defaultValue, sint32 minVal, sint32 maxVal) { auto option_value = iniParser->FindOption(optionName); option->isPresent = false; if (!option_value) { option->value = defaultValue; return false; } // parse option sint32 val = StringHelpers::ToInt(*option_value, defaultValue); if (val < minVal || val > maxVal) { cemuLog_log(LogType::Force, "Value '{}' is out of range for option '{}' in game profile", *option_value, optionName); option->value = defaultValue; return false; } option->isPresent = true; option->value = val; return true; } template bool gameProfile_loadIntegerOption(IniParser& iniParser, const char* optionName, T& option, T minVal, T maxVal) { static_assert(std::is_integral::value); auto option_value = iniParser.FindOption(optionName); if (!option_value) return false; // parse option try { T val = ConvertString(*option_value); if (val < minVal || val > maxVal) { cemuLog_log(LogType::Force, "Value '{}' is out of range for option '{}' in game profile", *option_value, optionName); return false; } option = val; return true; } catch(std::exception&) { cemuLog_log(LogType::Force, "Value '{}' is out of range for option '{}' in game profile", *option_value, optionName); return false; } } template bool gameProfile_loadEnumOption(IniParser& iniParser, const char* optionName, T& option) { static_assert(std::is_enum::value); auto option_value = iniParser.FindOption(optionName); if (!option_value) return false; for(const T& v : T()) { // test integer option if (boost::iequals(fmt::format("{}", static_cast::type>(v)), *option_value)) { option = v; return true; } // test enum name if(boost::iequals(fmt::format("{}", v), *option_value)) { option = v; return true; } } return false; } template bool gameProfile_loadEnumOption(IniParser& iniParser, const char* optionName, std::optional& option) { T tmp; const auto result = gameProfile_loadEnumOption(iniParser, optionName, tmp); if(result) option = tmp; return result; } void gameProfile_load() { g_current_game_profile->ResetOptional(); // reset with global values as optional g_current_game_profile->Load(CafeSystem::GetForegroundTitleId()); // apply some settings immediately ppcThreadQuantum = g_current_game_profile->GetThreadQuantum(); if (ppcThreadQuantum != GameProfile::kThreadQuantumDefault) cemuLog_log(LogType::Force, "Thread quantum set to {}", ppcThreadQuantum); } bool GameProfile::Load(uint64_t title_id) { auto gameProfilePath = ActiveSettings::GetConfigPath("gameProfiles/{:016x}.ini", title_id); std::optional> profileContents = FileStream::LoadIntoMemory(gameProfilePath); if (!profileContents) { gameProfilePath = ActiveSettings::GetDataPath("gameProfiles/default/{:016x}.ini", title_id); profileContents = FileStream::LoadIntoMemory(gameProfilePath); if (!profileContents) return false; m_is_default = true; } else m_is_default = false; m_is_loaded = true; // most official gameprofiles start with "# gamename" std::vector game_name; if (profileContents->size() > 0 && profileContents->data()[0] == '#') { char c; size_t idx = 1; while (idx < profileContents->size() && (c = profileContents->data()[idx]) != '\n' && idx < 128) { game_name.emplace_back(c); idx++; } m_gameName = std::string(game_name.begin(), game_name.end()); trim(m_gameName.value()); } IniParser iniParser(*profileContents, _pathToUtf8(gameProfilePath)); // parse ini while (iniParser.NextSection()) { if (boost::iequals(iniParser.GetCurrentSectionName(), "General")) { gameProfile_loadBooleanOption2(iniParser, "loadSharedLibraries", m_loadSharedLibraries); gameProfile_loadBooleanOption2(iniParser, "startWithPadView", m_startWithPadView); } else if (boost::iequals(iniParser.GetCurrentSectionName(), "Graphics")) { gameProfileIntegerOption_t graphicsApi; gameProfile_loadIntegerOption(&iniParser, "graphics_api", &graphicsApi, -1, 0, 1); if (graphicsApi.value != -1) m_graphics_api = (GraphicAPI)graphicsApi.value; gameProfile_loadEnumOption(iniParser, "accurateShaderMul", m_accurateShaderMul); // legacy support auto option_precompiledShaders = iniParser.FindOption("precompiledShaders"); if (option_precompiledShaders) { if (boost::iequals(*option_precompiledShaders, "1") || boost::iequals(*option_precompiledShaders, "true")) m_precompiledShaders = PrecompiledShaderOption::Enable; else if (boost::iequals(*option_precompiledShaders, "0") || boost::iequals(*option_precompiledShaders, "false")) m_precompiledShaders = PrecompiledShaderOption::Disable; else m_precompiledShaders = PrecompiledShaderOption::Auto; } else m_precompiledShaders = PrecompiledShaderOption::Auto; } else if (boost::iequals(iniParser.GetCurrentSectionName(), "Audio")) { gameProfile_loadBooleanOption2(iniParser, "disableAudio", m_disableAudio); } else if (boost::iequals(iniParser.GetCurrentSectionName(), "CPU")) { gameProfile_loadIntegerOption(iniParser, "threadQuantum", m_threadQuantum, 1000U, 536870912U); if (!gameProfile_loadEnumOption(iniParser, "cpuMode", m_cpuMode)) { // try to load the old enum value strings std::optional cpu_mode_legacy; if (gameProfile_loadEnumOption(iniParser, "cpuMode", cpu_mode_legacy) && cpu_mode_legacy.has_value()) { m_cpuMode = (CPUMode)cpu_mode_legacy.value(); if (m_cpuMode == CPUMode::DualcoreRecompiler) m_cpuMode = CPUMode::MulticoreRecompiler; } } } else if (boost::iequals(iniParser.GetCurrentSectionName(), "Controller")) { for (int i = 0; i < 8; ++i) { auto option_value = iniParser.FindOption(fmt::format("controller{}", (i + 1))); if (option_value) m_controllerProfile[i] = std::string(*option_value); } } } return true; } void GameProfile::Save(uint64_t title_id) { auto gameProfileDir = ActiveSettings::GetConfigPath("gameProfiles"); if (std::error_code ex_ec; !fs::exists(gameProfileDir, ex_ec)) fs::create_directories(gameProfileDir, ex_ec); auto gameProfilePath = gameProfileDir / fmt::format("{:016x}.ini", title_id); FileStream* fs = FileStream::createFile2(gameProfilePath); if (!fs) { cemuLog_log(LogType::Force, "Failed to write game profile"); return; } if (m_gameName) fs->writeLine(fmt::format("# {}\n", m_gameName.value()).c_str()); #define WRITE_OPTIONAL_ENTRY(__NAME) if (m_##__NAME) fs->writeLine(fmt::format("{} = {}", #__NAME, m_##__NAME.value()).c_str()); #define WRITE_ENTRY(__NAME) fs->writeLine(fmt::format("{} = {}", #__NAME, m_##__NAME).c_str()); fs->writeLine("[General]"); WRITE_OPTIONAL_ENTRY(loadSharedLibraries); WRITE_ENTRY(startWithPadView); fs->writeLine(""); fs->writeLine("[CPU]"); WRITE_OPTIONAL_ENTRY(cpuMode); WRITE_ENTRY(threadQuantum); fs->writeLine(""); fs->writeLine("[Graphics]"); WRITE_ENTRY(accurateShaderMul); WRITE_OPTIONAL_ENTRY(precompiledShaders); WRITE_OPTIONAL_ENTRY(graphics_api); fs->writeLine(""); fs->writeLine("[Controller]"); for (int i = 0; i < 8; ++i) { if (m_controllerProfile[i]) fs->writeLine(fmt::format("controller{} = {}", (i + 1), m_controllerProfile[i].value()).c_str()); } fs->writeLine(""); #undef WRITE_OPTIONAL_ENTRY #undef WRITE_ENTRY delete fs; } void GameProfile::ResetOptional() { m_gameName.reset(); // general settings m_loadSharedLibraries.reset(); // true; m_startWithPadView = false; // graphic settings m_accurateShaderMul = AccurateShaderMulOption::True; // cpu settings m_threadQuantum = kThreadQuantumDefault; m_cpuMode.reset(); // CPUModeOption::kSingleCoreRecompiler; // audio m_disableAudio = false; // controller settings for (auto& profile : m_controllerProfile) profile.reset(); } void GameProfile::Reset() { m_gameName.reset(); // general settings m_loadSharedLibraries = true; m_startWithPadView = false; // graphic settings m_accurateShaderMul = AccurateShaderMulOption::True; m_precompiledShaders = PrecompiledShaderOption::Auto; // cpu settings m_threadQuantum = kThreadQuantumDefault; m_cpuMode = CPUMode::Auto; // audio m_disableAudio = false; // controller settings for (auto& profile : m_controllerProfile) profile.reset(); }