376 lines
14 KiB
C++
376 lines
14 KiB
C++
// Copyright 2016 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
#include "Core/ConfigLoaders/GameConfigLoader.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <list>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/Config/Config.h"
|
|
#include "Common/FileUtil.h"
|
|
#include "Common/IniFile.h"
|
|
#include "Common/Logging/Log.h"
|
|
#include "Common/MsgHandler.h"
|
|
#include "Common/StringUtil.h"
|
|
|
|
#include "Core/Config/Config.h"
|
|
#include "Core/Config/GraphicsSettings.h"
|
|
#include "Core/ConfigLoaders/IsSettingSaveable.h"
|
|
|
|
namespace ConfigLoaders
|
|
{
|
|
// Returns all possible filenames in ascending order of priority
|
|
std::vector<std::string> GetGameIniFilenames(const std::string& id, std::optional<u16> revision)
|
|
{
|
|
std::vector<std::string> filenames;
|
|
|
|
if (id.empty())
|
|
return filenames;
|
|
|
|
// INIs that match the system code (unique for each Virtual Console system)
|
|
filenames.push_back(id.substr(0, 1) + ".ini");
|
|
|
|
// INIs that match all regions
|
|
if (id.size() >= 4)
|
|
filenames.push_back(id.substr(0, 3) + ".ini");
|
|
|
|
// Regular INIs
|
|
filenames.push_back(id + ".ini");
|
|
|
|
// INIs with specific revisions
|
|
if (revision)
|
|
filenames.push_back(id + StringFromFormat("r%d", *revision) + ".ini");
|
|
|
|
return filenames;
|
|
}
|
|
|
|
using ConfigLocation = Config::ConfigLocation;
|
|
using INIToLocationMap = std::map<std::pair<std::string, std::string>, ConfigLocation>;
|
|
|
|
// This is a mapping from the legacy section-key pairs to ConfigLocations.
|
|
// New settings do not need to be added to this mapping.
|
|
// See also: MapINIToRealLocation and GetINILocationFromConfig.
|
|
static const INIToLocationMap& GetINIToLocationMap()
|
|
{
|
|
static const INIToLocationMap ini_to_location = {
|
|
{{"Video_Hardware", "VSync"}, {Config::GFX_VSYNC.location}},
|
|
|
|
{{"Video_Settings", "wideScreenHack"}, {Config::GFX_WIDESCREEN_HACK.location}},
|
|
{{"Video_Settings", "AspectRatio"}, {Config::GFX_ASPECT_RATIO.location}},
|
|
{{"Video_Settings", "Crop"}, {Config::GFX_CROP.location}},
|
|
{{"Video_Settings", "UseXFB"}, {Config::GFX_USE_XFB.location}},
|
|
{{"Video_Settings", "UseRealXFB"}, {Config::GFX_USE_REAL_XFB.location}},
|
|
{{"Video_Settings", "SafeTextureCacheColorSamples"},
|
|
{Config::GFX_SAFE_TEXTURE_CACHE_COLOR_SAMPLES.location}},
|
|
{{"Video_Settings", "HiresTextures"}, {Config::GFX_HIRES_TEXTURES.location}},
|
|
{{"Video_Settings", "ConvertHiresTextures"}, {Config::GFX_CONVERT_HIRES_TEXTURES.location}},
|
|
{{"Video_Settings", "CacheHiresTextures"}, {Config::GFX_CACHE_HIRES_TEXTURES.location}},
|
|
{{"Video_Settings", "EnablePixelLighting"}, {Config::GFX_ENABLE_PIXEL_LIGHTING.location}},
|
|
{{"Video_Settings", "FastDepthCalc"}, {Config::GFX_FAST_DEPTH_CALC.location}},
|
|
{{"Video_Settings", "MSAA"}, {Config::GFX_MSAA.location}},
|
|
{{"Video_Settings", "SSAA"}, {Config::GFX_SSAA.location}},
|
|
{{"Video_Settings", "ForceTrueColor"}, {Config::GFX_ENHANCE_FORCE_TRUE_COLOR.location}},
|
|
{{"Video_Settings", "EFBScale"}, {Config::GFX_EFB_SCALE.location}},
|
|
{{"Video_Settings", "DisableFog"}, {Config::GFX_DISABLE_FOG.location}},
|
|
{{"Video_Settings", "BackendMultithreading"}, {Config::GFX_BACKEND_MULTITHREADING.location}},
|
|
{{"Video_Settings", "CommandBufferExecuteInterval"},
|
|
{Config::GFX_COMMAND_BUFFER_EXECUTE_INTERVAL.location}},
|
|
|
|
{{"Video_Enhancements", "ForceFiltering"}, {Config::GFX_ENHANCE_FORCE_FILTERING.location}},
|
|
{{"Video_Enhancements", "MaxAnisotropy"}, {Config::GFX_ENHANCE_MAX_ANISOTROPY.location}},
|
|
{{"Video_Enhancements", "PostProcessingShader"}, {Config::GFX_ENHANCE_POST_SHADER.location}},
|
|
|
|
{{"Video_Stereoscopy", "StereoConvergence"}, {Config::GFX_STEREO_CONVERGENCE.location}},
|
|
{{"Video_Stereoscopy", "StereoEFBMonoDepth"}, {Config::GFX_STEREO_EFB_MONO_DEPTH.location}},
|
|
{{"Video_Stereoscopy", "StereoDepthPercentage"},
|
|
{Config::GFX_STEREO_DEPTH_PERCENTAGE.location}},
|
|
|
|
{{"Video_Stereoscopy", "StereoMode"}, {Config::GFX_STEREO_MODE.location}},
|
|
{{"Video_Stereoscopy", "StereoDepth"}, {Config::GFX_STEREO_DEPTH.location}},
|
|
{{"Video_Stereoscopy", "StereoSwapEyes"}, {Config::GFX_STEREO_SWAP_EYES.location}},
|
|
|
|
{{"Video_Hacks", "EFBAccessEnable"}, {Config::GFX_HACK_EFB_ACCESS_ENABLE.location}},
|
|
{{"Video_Hacks", "BBoxEnable"}, {Config::GFX_HACK_BBOX_ENABLE.location}},
|
|
{{"Video_Hacks", "ForceProgressive"}, {Config::GFX_HACK_FORCE_PROGRESSIVE.location}},
|
|
{{"Video_Hacks", "EFBToTextureEnable"}, {Config::GFX_HACK_SKIP_EFB_COPY_TO_RAM.location}},
|
|
{{"Video_Hacks", "EFBScaledCopy"}, {Config::GFX_EFB_SCALE.location}},
|
|
{{"Video_Hacks", "EFBEmulateFormatChanges"},
|
|
{Config::GFX_HACK_EFB_EMULATE_FORMAT_CHANGES.location}},
|
|
{{"Video_Hacks", "VertexRounding"}, {Config::GFX_HACK_VERTEX_ROUDING.location}},
|
|
|
|
{{"Video", "ProjectionHack"}, {Config::GFX_PROJECTION_HACK.location}},
|
|
{{"Video", "PH_SZNear"}, {Config::GFX_PROJECTION_HACK_SZNEAR.location}},
|
|
{{"Video", "PH_SZFar"}, {Config::GFX_PROJECTION_HACK_SZFAR.location}},
|
|
{{"Video", "PH_ZNear"}, {Config::GFX_PROJECTION_HACK_ZNEAR.location}},
|
|
{{"Video", "PH_ZFar"}, {Config::GFX_PROJECTION_HACK_ZFAR.location}},
|
|
{{"Video", "PerfQueriesEnable"}, {Config::GFX_PERF_QUERIES_ENABLE.location}},
|
|
};
|
|
return ini_to_location;
|
|
}
|
|
|
|
// Converts from a legacy GameINI section-key tuple to a ConfigLocation.
|
|
// Also supports the following format:
|
|
// [System.Section]
|
|
// Key = Value
|
|
static ConfigLocation MapINIToRealLocation(const std::string& section, const std::string& key)
|
|
{
|
|
static const INIToLocationMap& ini_to_location = GetINIToLocationMap();
|
|
|
|
auto it = ini_to_location.find({section, key});
|
|
if (it == ini_to_location.end())
|
|
{
|
|
// Try again, but this time with an empty key
|
|
// Certain sections like 'Speedhacks' has keys that are variable
|
|
it = ini_to_location.find({section, ""});
|
|
if (it != ini_to_location.end())
|
|
return {it->second.system, it->second.section, key};
|
|
|
|
// Attempt to load it as a configuration option
|
|
// It will be in the format of '<System>.<Section>'
|
|
std::istringstream buffer(section);
|
|
std::string system_str, config_section;
|
|
|
|
bool fail = false;
|
|
std::getline(buffer, system_str, '.');
|
|
fail |= buffer.fail();
|
|
std::getline(buffer, config_section, '.');
|
|
fail |= buffer.fail();
|
|
|
|
if (!fail)
|
|
return {Config::GetSystemFromName(system_str), config_section, key};
|
|
|
|
WARN_LOG(CORE, "Unknown game INI option in section %s: %s", section.c_str(), key.c_str());
|
|
return {Config::System::Main, "", ""};
|
|
}
|
|
|
|
return ini_to_location.at({section, key});
|
|
}
|
|
|
|
static std::pair<std::string, std::string> GetINILocationFromConfig(const ConfigLocation& location)
|
|
{
|
|
static const INIToLocationMap& ini_to_location = GetINIToLocationMap();
|
|
|
|
auto it = std::find_if(ini_to_location.begin(), ini_to_location.end(),
|
|
[&location](const auto& entry) { return entry.second == location; });
|
|
|
|
if (it != ini_to_location.end())
|
|
return it->first;
|
|
|
|
// Try again, but this time with an empty key
|
|
// Certain sections like 'Speedhacks' have keys that are variable
|
|
it = std::find_if(ini_to_location.begin(), ini_to_location.end(), [&location](const auto& entry) {
|
|
return std::tie(entry.second.system, entry.second.section) ==
|
|
std::tie(location.system, location.section);
|
|
});
|
|
if (it != ini_to_location.end())
|
|
return {it->first.first, location.key};
|
|
|
|
return {Config::GetSystemName(location.system) + "." + location.section, location.key};
|
|
}
|
|
|
|
// INI Game layer configuration loader
|
|
class INIGameConfigLayerLoader final : public Config::ConfigLayerLoader
|
|
{
|
|
public:
|
|
INIGameConfigLayerLoader(const std::string& id, u16 revision, bool global)
|
|
: ConfigLayerLoader(global ? Config::LayerType::GlobalGame : Config::LayerType::LocalGame),
|
|
m_id(id), m_revision(revision)
|
|
{
|
|
}
|
|
|
|
void Load(Config::Layer* config_layer) override
|
|
{
|
|
IniFile ini;
|
|
if (config_layer->GetLayer() == Config::LayerType::GlobalGame)
|
|
{
|
|
for (const std::string& filename : GetGameIniFilenames(m_id, m_revision))
|
|
ini.Load(File::GetSysDirectory() + GAMESETTINGS_DIR DIR_SEP + filename, true);
|
|
}
|
|
else
|
|
{
|
|
for (const std::string& filename : GetGameIniFilenames(m_id, m_revision))
|
|
ini.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + filename, true);
|
|
}
|
|
|
|
const std::list<IniFile::Section>& system_sections = ini.GetSections();
|
|
|
|
for (const auto& section : system_sections)
|
|
{
|
|
LoadFromSystemSection(config_layer, section);
|
|
}
|
|
|
|
LoadControllerConfig(config_layer);
|
|
}
|
|
|
|
void Save(Config::Layer* config_layer) override;
|
|
|
|
private:
|
|
void LoadControllerConfig(Config::Layer* config_layer) const
|
|
{
|
|
// Game INIs can have controller profiles embedded in to them
|
|
static const std::array<char, 4> nums = {{'1', '2', '3', '4'}};
|
|
|
|
if (m_id == "00000000")
|
|
return;
|
|
|
|
const std::array<std::tuple<std::string, std::string, Config::System>, 2> profile_info = {{
|
|
std::make_tuple("Pad", "GCPad", Config::System::GCPad),
|
|
std::make_tuple("Wiimote", "Wiimote", Config::System::WiiPad),
|
|
}};
|
|
|
|
for (const auto& use_data : profile_info)
|
|
{
|
|
std::string type = std::get<0>(use_data);
|
|
std::string path = "Profiles/" + std::get<1>(use_data) + "/";
|
|
|
|
Config::Section* control_section =
|
|
config_layer->GetOrCreateSection(std::get<2>(use_data), "Controls");
|
|
|
|
for (const char num : nums)
|
|
{
|
|
bool use_profile = false;
|
|
std::string profile;
|
|
if (control_section->Exists(type + "Profile" + num))
|
|
{
|
|
if (control_section->Get(type + "Profile" + num, &profile))
|
|
{
|
|
if (File::Exists(File::GetUserPath(D_CONFIG_IDX) + path + profile + ".ini"))
|
|
{
|
|
use_profile = true;
|
|
}
|
|
else
|
|
{
|
|
// TODO: PanicAlert shouldn't be used for this.
|
|
PanicAlertT("Selected controller profile does not exist");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (use_profile)
|
|
{
|
|
IniFile profile_ini;
|
|
profile_ini.Load(File::GetUserPath(D_CONFIG_IDX) + path + profile + ".ini");
|
|
|
|
const IniFile::Section* ini_section = profile_ini.GetOrCreateSection("Profile");
|
|
const IniFile::Section::SectionMap& section_map = ini_section->GetValues();
|
|
for (const auto& value : section_map)
|
|
{
|
|
Config::Section* section = config_layer->GetOrCreateSection(
|
|
std::get<2>(use_data), std::get<1>(use_data) + num);
|
|
section->Set(value.first, value.second);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LoadFromSystemSection(Config::Layer* config_layer, const IniFile::Section& section) const
|
|
{
|
|
const std::string section_name = section.GetName();
|
|
if (section.HasLines())
|
|
{
|
|
// Trash INI File chunks
|
|
std::vector<std::string> chunk;
|
|
section.GetLines(&chunk, true);
|
|
|
|
if (chunk.size())
|
|
{
|
|
const auto mapped_config = MapINIToRealLocation(section_name, "");
|
|
|
|
if (mapped_config.section.empty() && mapped_config.key.empty())
|
|
return;
|
|
|
|
auto* config_section =
|
|
config_layer->GetOrCreateSection(mapped_config.system, mapped_config.section);
|
|
config_section->SetLines(chunk);
|
|
}
|
|
}
|
|
|
|
// Regular key,value pairs
|
|
const IniFile::Section::SectionMap& section_map = section.GetValues();
|
|
|
|
for (const auto& value : section_map)
|
|
{
|
|
const auto mapped_config = MapINIToRealLocation(section_name, value.first);
|
|
|
|
if (mapped_config.section.empty() && mapped_config.key.empty())
|
|
continue;
|
|
|
|
auto* config_section =
|
|
config_layer->GetOrCreateSection(mapped_config.system, mapped_config.section);
|
|
config_section->Set(mapped_config.key, value.second);
|
|
}
|
|
}
|
|
|
|
const std::string m_id;
|
|
const u16 m_revision;
|
|
};
|
|
|
|
void INIGameConfigLayerLoader::Save(Config::Layer* config_layer)
|
|
{
|
|
if (config_layer->GetLayer() != Config::LayerType::LocalGame)
|
|
return;
|
|
|
|
IniFile ini;
|
|
for (const std::string& file_name : GetGameIniFilenames(m_id, m_revision))
|
|
ini.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + file_name, true);
|
|
|
|
for (const auto& system : config_layer->GetLayerMap())
|
|
{
|
|
for (const auto& section : system.second)
|
|
{
|
|
for (const auto& value : section->GetValues())
|
|
{
|
|
if (!IsSettingSaveable({system.first, section->GetName(), value.first}))
|
|
continue;
|
|
|
|
const auto ini_location =
|
|
GetINILocationFromConfig({system.first, section->GetName(), value.first});
|
|
if (ini_location.first.empty() && ini_location.second.empty())
|
|
continue;
|
|
|
|
IniFile::Section* ini_section = ini.GetOrCreateSection(ini_location.first);
|
|
ini_section->Set(ini_location.second, value.second);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to save to the revision specific INI first, if it exists.
|
|
const std::string gameini_with_rev =
|
|
File::GetUserPath(D_GAMESETTINGS_IDX) + m_id + StringFromFormat("r%d", m_revision) + ".ini";
|
|
if (File::Exists(gameini_with_rev))
|
|
{
|
|
ini.Save(gameini_with_rev);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, save to the game INI. We don't try any INI broader than that because it will
|
|
// likely cause issues with cheat codes and game patches.
|
|
const std::string gameini = File::GetUserPath(D_GAMESETTINGS_IDX) + m_id + ".ini";
|
|
ini.Save(gameini);
|
|
}
|
|
|
|
// Loader generation
|
|
std::unique_ptr<Config::ConfigLayerLoader> GenerateGlobalGameConfigLoader(const std::string& id,
|
|
u16 revision)
|
|
{
|
|
return std::make_unique<INIGameConfigLayerLoader>(id, revision, true);
|
|
}
|
|
|
|
std::unique_ptr<Config::ConfigLayerLoader> GenerateLocalGameConfigLoader(const std::string& id,
|
|
u16 revision)
|
|
{
|
|
return std::make_unique<INIGameConfigLayerLoader>(id, revision, false);
|
|
}
|
|
}
|