337 lines
12 KiB
C++
337 lines
12 KiB
C++
// Copyright 2016 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#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 <fmt/format.h>
|
|
|
|
#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/MainSettings.h"
|
|
#include "Core/Config/SYSCONFSettings.h"
|
|
#include "Core/Config/WiimoteSettings.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;
|
|
|
|
// Using the first letter or the 3 letters of the ID only makes sense
|
|
// if the ID is an actual game ID (which has 6 characters).
|
|
if (id.length() == 6)
|
|
{
|
|
// 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
|
|
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 + fmt::format("r{}", *revision) + ".ini");
|
|
|
|
return filenames;
|
|
}
|
|
|
|
using Location = Config::Location;
|
|
using INIToLocationMap = std::map<std::pair<std::string, std::string>, Location>;
|
|
using INIToSectionMap = std::map<std::string, std::pair<Config::System, std::string>>;
|
|
|
|
// This is a mapping from the legacy section-key pairs to Locations.
|
|
// 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 = {
|
|
{{"Core", "ProgressiveScan"}, {Config::SYSCONF_PROGRESSIVE_SCAN.GetLocation()}},
|
|
{{"Core", "PAL60"}, {Config::SYSCONF_PAL60.GetLocation()}},
|
|
{{"Wii", "Widescreen"}, {Config::SYSCONF_WIDESCREEN.GetLocation()}},
|
|
{{"Wii", "Language"}, {Config::SYSCONF_LANGUAGE.GetLocation()}},
|
|
{{"Core", "HLE_BS2"}, {Config::MAIN_SKIP_IPL.GetLocation()}},
|
|
{{"Core", "GameCubeLanguage"}, {Config::MAIN_GC_LANGUAGE.GetLocation()}},
|
|
{{"Controls", "PadType0"}, {Config::GetInfoForSIDevice(0).GetLocation()}},
|
|
{{"Controls", "PadType1"}, {Config::GetInfoForSIDevice(1).GetLocation()}},
|
|
{{"Controls", "PadType2"}, {Config::GetInfoForSIDevice(2).GetLocation()}},
|
|
{{"Controls", "PadType3"}, {Config::GetInfoForSIDevice(3).GetLocation()}},
|
|
{{"Controls", "WiimoteSource0"}, {Config::WIIMOTE_1_SOURCE.GetLocation()}},
|
|
{{"Controls", "WiimoteSource1"}, {Config::WIIMOTE_2_SOURCE.GetLocation()}},
|
|
{{"Controls", "WiimoteSource2"}, {Config::WIIMOTE_3_SOURCE.GetLocation()}},
|
|
{{"Controls", "WiimoteSource3"}, {Config::WIIMOTE_4_SOURCE.GetLocation()}},
|
|
{{"Controls", "WiimoteSourceBB"}, {Config::WIIMOTE_BB_SOURCE.GetLocation()}},
|
|
};
|
|
return ini_to_location;
|
|
}
|
|
|
|
// This is a mapping from the legacy section names to system + section.
|
|
// New settings do not need to be added to this mapping.
|
|
// See also: MapINIToRealLocation and GetINILocationFromConfig.
|
|
static const INIToSectionMap& GetINIToSectionMap()
|
|
{
|
|
static const INIToSectionMap ini_to_section = {
|
|
{"Core", {Config::System::Main, "Core"}},
|
|
{"DSP", {Config::System::Main, "DSP"}},
|
|
{"Display", {Config::System::Main, "Display"}},
|
|
{"Video_Hardware", {Config::System::GFX, "Hardware"}},
|
|
{"Video_Settings", {Config::System::GFX, "Settings"}},
|
|
{"Video_Enhancements", {Config::System::GFX, "Enhancements"}},
|
|
{"Video_Stereoscopy", {Config::System::GFX, "Stereoscopy"}},
|
|
{"Video_Hacks", {Config::System::GFX, "Hacks"}},
|
|
{"Video", {Config::System::GFX, "GameSpecific"}},
|
|
};
|
|
return ini_to_section;
|
|
}
|
|
|
|
// Converts from a legacy GameINI section-key tuple to a Location.
|
|
// Also supports the following format:
|
|
// [System.Section]
|
|
// Key = Value
|
|
static Location MapINIToRealLocation(const std::string& section, const std::string& key)
|
|
{
|
|
static const INIToLocationMap& ini_to_location = GetINIToLocationMap();
|
|
const auto it = ini_to_location.find({section, key});
|
|
if (it != ini_to_location.end())
|
|
return it->second;
|
|
|
|
static const INIToSectionMap& ini_to_section = GetINIToSectionMap();
|
|
const auto it2 = ini_to_section.find(section);
|
|
if (it2 != ini_to_section.end())
|
|
return {it2->second.first, it2->second.second, 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();
|
|
|
|
const std::optional<Config::System> system = Config::GetSystemFromName(system_str);
|
|
if (!fail && system)
|
|
return {*system, config_section, key};
|
|
|
|
WARN_LOG_FMT(CORE, "Unknown game INI option in section {}: {}", section, key);
|
|
return {Config::System::Main, "", ""};
|
|
}
|
|
|
|
static std::pair<std::string, std::string> GetINILocationFromConfig(const Location& location)
|
|
{
|
|
static const INIToLocationMap& ini_to_location = GetINIToLocationMap();
|
|
const 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;
|
|
|
|
static const INIToSectionMap& ini_to_section = GetINIToSectionMap();
|
|
const auto it2 =
|
|
std::find_if(ini_to_section.begin(), ini_to_section.end(), [&location](const auto& entry) {
|
|
return entry.second.first == location.system && entry.second.second == location.section;
|
|
});
|
|
if (it2 != ini_to_section.end())
|
|
return {it2->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* layer) override
|
|
{
|
|
IniFile ini;
|
|
if (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(layer, section);
|
|
}
|
|
|
|
LoadControllerConfig(layer);
|
|
}
|
|
|
|
void Save(Config::Layer* layer) override;
|
|
|
|
private:
|
|
void LoadControllerConfig(Config::Layer* 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) + "/";
|
|
|
|
const auto control_section = [&](std::string key) {
|
|
return Config::Location{std::get<2>(use_data), "Controls", key};
|
|
};
|
|
|
|
for (const char num : nums)
|
|
{
|
|
if (auto profile = layer->Get<std::string>(control_section(type + "Profile" + num)))
|
|
{
|
|
std::string ini_path = File::GetUserPath(D_CONFIG_IDX) + path + *profile + ".ini";
|
|
if (!File::Exists(ini_path))
|
|
{
|
|
// TODO: PanicAlert shouldn't be used for this.
|
|
PanicAlertFmtT("Selected controller profile does not exist");
|
|
continue;
|
|
}
|
|
|
|
IniFile profile_ini;
|
|
profile_ini.Load(ini_path);
|
|
|
|
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::Location location{std::get<2>(use_data), std::get<1>(use_data) + num,
|
|
value.first};
|
|
layer->Set(location, value.second);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LoadFromSystemSection(Config::Layer* layer, const IniFile::Section& section) const
|
|
{
|
|
const std::string section_name = section.GetName();
|
|
|
|
// Regular key,value pairs
|
|
const IniFile::Section::SectionMap& section_map = section.GetValues();
|
|
|
|
for (const auto& value : section_map)
|
|
{
|
|
const auto location = MapINIToRealLocation(section_name, value.first);
|
|
|
|
if (location.section.empty() && location.key.empty())
|
|
continue;
|
|
|
|
if (location.system == Config::System::Session)
|
|
continue;
|
|
|
|
layer->Set(location, value.second);
|
|
}
|
|
}
|
|
|
|
const std::string m_id;
|
|
const u16 m_revision;
|
|
};
|
|
|
|
void INIGameConfigLayerLoader::Save(Config::Layer* layer)
|
|
{
|
|
if (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& config : layer->GetLayerMap())
|
|
{
|
|
const Config::Location& location = config.first;
|
|
const std::optional<std::string>& value = config.second;
|
|
|
|
if (!IsSettingSaveable(location) || location.system == Config::System::Session)
|
|
continue;
|
|
|
|
const auto ini_location = GetINILocationFromConfig(location);
|
|
if (ini_location.first.empty() && ini_location.second.empty())
|
|
continue;
|
|
|
|
if (value)
|
|
{
|
|
IniFile::Section* ini_section = ini.GetOrCreateSection(ini_location.first);
|
|
ini_section->Set(ini_location.second, *value);
|
|
}
|
|
else
|
|
{
|
|
ini.DeleteKey(ini_location.first, ini_location.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 + fmt::format("r{}", 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);
|
|
}
|
|
} // namespace ConfigLoaders
|