1 // Copyright 2016 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "Core/ConfigLoaders/GameConfigLoader.h"
6
7 #include <algorithm>
8 #include <array>
9 #include <list>
10 #include <map>
11 #include <optional>
12 #include <sstream>
13 #include <string>
14 #include <tuple>
15 #include <utility>
16 #include <vector>
17
18 #include <fmt/format.h>
19
20 #include "Common/CommonPaths.h"
21 #include "Common/Config/Config.h"
22 #include "Common/FileUtil.h"
23 #include "Common/IniFile.h"
24 #include "Common/Logging/Log.h"
25 #include "Common/MsgHandler.h"
26 #include "Common/StringUtil.h"
27
28 #include "Core/Config/SYSCONFSettings.h"
29 #include "Core/ConfigLoaders/IsSettingSaveable.h"
30
31 namespace ConfigLoaders
32 {
33 // Returns all possible filenames in ascending order of priority
GetGameIniFilenames(const std::string & id,std::optional<u16> revision)34 std::vector<std::string> GetGameIniFilenames(const std::string& id, std::optional<u16> revision)
35 {
36 std::vector<std::string> filenames;
37
38 if (id.empty())
39 return filenames;
40
41 // Using the first letter or the 3 letters of the ID only makes sense
42 // if the ID is an actual game ID (which has 6 characters).
43 if (id.length() == 6)
44 {
45 // INIs that match the system code (unique for each Virtual Console system)
46 filenames.push_back(id.substr(0, 1) + ".ini");
47
48 // INIs that match all regions
49 filenames.push_back(id.substr(0, 3) + ".ini");
50 }
51
52 // Regular INIs
53 filenames.push_back(id + ".ini");
54
55 // INIs with specific revisions
56 if (revision)
57 filenames.push_back(id + fmt::format("r{}", *revision) + ".ini");
58
59 return filenames;
60 }
61
62 using Location = Config::Location;
63 using INIToLocationMap = std::map<std::pair<std::string, std::string>, Location>;
64 using INIToSectionMap = std::map<std::string, std::pair<Config::System, std::string>>;
65
66 // This is a mapping from the legacy section-key pairs to Locations.
67 // New settings do not need to be added to this mapping.
68 // See also: MapINIToRealLocation and GetINILocationFromConfig.
GetINIToLocationMap()69 static const INIToLocationMap& GetINIToLocationMap()
70 {
71 static const INIToLocationMap ini_to_location = {
72 {{"Core", "ProgressiveScan"}, {Config::SYSCONF_PROGRESSIVE_SCAN.location}},
73 {{"Core", "PAL60"}, {Config::SYSCONF_PAL60.location}},
74 {{"Wii", "Widescreen"}, {Config::SYSCONF_WIDESCREEN.location}},
75 {{"Wii", "Language"}, {Config::SYSCONF_LANGUAGE.location}},
76 };
77 return ini_to_location;
78 }
79
80 // This is a mapping from the legacy section names to system + section.
81 // New settings do not need to be added to this mapping.
82 // See also: MapINIToRealLocation and GetINILocationFromConfig.
GetINIToSectionMap()83 static const INIToSectionMap& GetINIToSectionMap()
84 {
85 static const INIToSectionMap ini_to_section = {
86 {"Core", {Config::System::Main, "Core"}},
87 {"Display", {Config::System::Main, "Display"}},
88 {"Video_Hardware", {Config::System::GFX, "Hardware"}},
89 {"Video_Settings", {Config::System::GFX, "Settings"}},
90 {"Video_Enhancements", {Config::System::GFX, "Enhancements"}},
91 {"Video_Stereoscopy", {Config::System::GFX, "Stereoscopy"}},
92 {"Video_Hacks", {Config::System::GFX, "Hacks"}},
93 {"Video", {Config::System::GFX, "GameSpecific"}},
94 };
95 return ini_to_section;
96 }
97
98 // Converts from a legacy GameINI section-key tuple to a Location.
99 // Also supports the following format:
100 // [System.Section]
101 // Key = Value
MapINIToRealLocation(const std::string & section,const std::string & key)102 static Location MapINIToRealLocation(const std::string& section, const std::string& key)
103 {
104 static const INIToLocationMap& ini_to_location = GetINIToLocationMap();
105 const auto it = ini_to_location.find({section, key});
106 if (it != ini_to_location.end())
107 return it->second;
108
109 static const INIToSectionMap& ini_to_section = GetINIToSectionMap();
110 const auto it2 = ini_to_section.find(section);
111 if (it2 != ini_to_section.end())
112 return {it2->second.first, it2->second.second, key};
113
114 // Attempt to load it as a configuration option
115 // It will be in the format of '<System>.<Section>'
116 std::istringstream buffer(section);
117 std::string system_str, config_section;
118
119 bool fail = false;
120 std::getline(buffer, system_str, '.');
121 fail |= buffer.fail();
122 std::getline(buffer, config_section, '.');
123 fail |= buffer.fail();
124
125 const std::optional<Config::System> system = Config::GetSystemFromName(system_str);
126 if (!fail && system)
127 return {*system, config_section, key};
128
129 WARN_LOG(CORE, "Unknown game INI option in section %s: %s", section.c_str(), key.c_str());
130 return {Config::System::Main, "", ""};
131 }
132
GetINILocationFromConfig(const Location & location)133 static std::pair<std::string, std::string> GetINILocationFromConfig(const Location& location)
134 {
135 static const INIToLocationMap& ini_to_location = GetINIToLocationMap();
136 const auto it = std::find_if(ini_to_location.begin(), ini_to_location.end(),
137 [&location](const auto& entry) { return entry.second == location; });
138 if (it != ini_to_location.end())
139 return it->first;
140
141 static const INIToSectionMap& ini_to_section = GetINIToSectionMap();
142 const auto it2 =
143 std::find_if(ini_to_section.begin(), ini_to_section.end(), [&location](const auto& entry) {
144 return entry.second.first == location.system && entry.second.second == location.section;
145 });
146 if (it2 != ini_to_section.end())
147 return {it2->first, location.key};
148
149 return {Config::GetSystemName(location.system) + "." + location.section, location.key};
150 }
151
152 // INI Game layer configuration loader
153 class INIGameConfigLayerLoader final : public Config::ConfigLayerLoader
154 {
155 public:
INIGameConfigLayerLoader(const std::string & id,u16 revision,bool global)156 INIGameConfigLayerLoader(const std::string& id, u16 revision, bool global)
157 : ConfigLayerLoader(global ? Config::LayerType::GlobalGame : Config::LayerType::LocalGame),
158 m_id(id), m_revision(revision)
159 {
160 }
161
Load(Config::Layer * layer)162 void Load(Config::Layer* layer) override
163 {
164 IniFile ini;
165 if (layer->GetLayer() == Config::LayerType::GlobalGame)
166 {
167 for (const std::string& filename : GetGameIniFilenames(m_id, m_revision))
168 ini.Load(File::GetSysDirectory() + GAMESETTINGS_DIR DIR_SEP + filename, true);
169 }
170 else
171 {
172 for (const std::string& filename : GetGameIniFilenames(m_id, m_revision))
173 ini.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + filename, true);
174 }
175
176 const std::list<IniFile::Section>& system_sections = ini.GetSections();
177
178 for (const auto& section : system_sections)
179 {
180 LoadFromSystemSection(layer, section);
181 }
182
183 LoadControllerConfig(layer);
184 }
185
186 void Save(Config::Layer* layer) override;
187
188 private:
LoadControllerConfig(Config::Layer * layer) const189 void LoadControllerConfig(Config::Layer* layer) const
190 {
191 // Game INIs can have controller profiles embedded in to them
192 static const std::array<char, 4> nums = {{'1', '2', '3', '4'}};
193
194 if (m_id == "00000000")
195 return;
196
197 const std::array<std::tuple<std::string, std::string, Config::System>, 2> profile_info = {{
198 std::make_tuple("Pad", "GCPad", Config::System::GCPad),
199 std::make_tuple("Wiimote", "Wiimote", Config::System::WiiPad),
200 }};
201
202 for (const auto& use_data : profile_info)
203 {
204 std::string type = std::get<0>(use_data);
205 std::string path = "Profiles/" + std::get<1>(use_data) + "/";
206
207 const auto control_section = [&](std::string key) {
208 return Config::Location{std::get<2>(use_data), "Controls", key};
209 };
210
211 for (const char num : nums)
212 {
213 if (auto profile = layer->Get<std::string>(control_section(type + "Profile" + num)))
214 {
215 std::string ini_path = File::GetUserPath(D_CONFIG_IDX) + path + *profile + ".ini";
216 if (!File::Exists(ini_path))
217 {
218 // TODO: PanicAlert shouldn't be used for this.
219 PanicAlertT("Selected controller profile does not exist");
220 continue;
221 }
222
223 IniFile profile_ini;
224 profile_ini.Load(ini_path);
225
226 const IniFile::Section* ini_section = profile_ini.GetOrCreateSection("Profile");
227 const IniFile::Section::SectionMap& section_map = ini_section->GetValues();
228 for (const auto& value : section_map)
229 {
230 Config::Location location{std::get<2>(use_data), std::get<1>(use_data) + num,
231 value.first};
232 layer->Set(location, value.second);
233 }
234 }
235 }
236 }
237 }
238
LoadFromSystemSection(Config::Layer * layer,const IniFile::Section & section) const239 void LoadFromSystemSection(Config::Layer* layer, const IniFile::Section& section) const
240 {
241 const std::string section_name = section.GetName();
242
243 // Regular key,value pairs
244 const IniFile::Section::SectionMap& section_map = section.GetValues();
245
246 for (const auto& value : section_map)
247 {
248 const auto location = MapINIToRealLocation(section_name, value.first);
249
250 if (location.section.empty() && location.key.empty())
251 continue;
252
253 layer->Set(location, value.second);
254 }
255 }
256
257 const std::string m_id;
258 const u16 m_revision;
259 };
260
Save(Config::Layer * layer)261 void INIGameConfigLayerLoader::Save(Config::Layer* layer)
262 {
263 if (layer->GetLayer() != Config::LayerType::LocalGame)
264 return;
265
266 IniFile ini;
267 for (const std::string& file_name : GetGameIniFilenames(m_id, m_revision))
268 ini.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + file_name, true);
269
270 for (const auto& config : layer->GetLayerMap())
271 {
272 const Config::Location& location = config.first;
273 const std::optional<std::string>& value = config.second;
274
275 if (!IsSettingSaveable(location))
276 continue;
277
278 const auto ini_location = GetINILocationFromConfig(location);
279 if (ini_location.first.empty() && ini_location.second.empty())
280 continue;
281
282 if (value)
283 {
284 IniFile::Section* ini_section = ini.GetOrCreateSection(ini_location.first);
285 ini_section->Set(ini_location.second, *value);
286 }
287 else
288 {
289 ini.DeleteKey(ini_location.first, ini_location.second);
290 }
291 }
292
293 // Try to save to the revision specific INI first, if it exists.
294 const std::string gameini_with_rev =
295 File::GetUserPath(D_GAMESETTINGS_IDX) + m_id + fmt::format("r{}", m_revision) + ".ini";
296 if (File::Exists(gameini_with_rev))
297 {
298 ini.Save(gameini_with_rev);
299 return;
300 }
301
302 // Otherwise, save to the game INI. We don't try any INI broader than that because it will
303 // likely cause issues with cheat codes and game patches.
304 const std::string gameini = File::GetUserPath(D_GAMESETTINGS_IDX) + m_id + ".ini";
305 ini.Save(gameini);
306 }
307
308 // Loader generation
GenerateGlobalGameConfigLoader(const std::string & id,u16 revision)309 std::unique_ptr<Config::ConfigLayerLoader> GenerateGlobalGameConfigLoader(const std::string& id,
310 u16 revision)
311 {
312 return std::make_unique<INIGameConfigLayerLoader>(id, revision, true);
313 }
314
GenerateLocalGameConfigLoader(const std::string & id,u16 revision)315 std::unique_ptr<Config::ConfigLayerLoader> GenerateLocalGameConfigLoader(const std::string& id,
316 u16 revision)
317 {
318 return std::make_unique<INIGameConfigLayerLoader>(id, revision, false);
319 }
320 } // namespace ConfigLoaders
321