added config mode support

This commit is contained in:
georgemoralis 2026-02-23 23:19:46 +02:00
parent a8f51584bf
commit faf9cce67e
2 changed files with 238 additions and 222 deletions

View File

@ -1,17 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <map>
#include <common/path_util.h> #include <common/path_util.h>
#include "common/logging/log.h" #include "common/logging/log.h"
#include "emulator_settings.h" #include "emulator_settings.h"
using json = nlohmann::json; using json = nlohmann::json;
// ── Singleton storage ─────────────────────────────────────────────────
std::shared_ptr<EmulatorSettings> EmulatorSettings::s_instance = nullptr; std::shared_ptr<EmulatorSettings> EmulatorSettings::s_instance = nullptr;
std::mutex EmulatorSettings::s_mutex; std::mutex EmulatorSettings::s_mutex;
// ── nlohmann helpers for std::filesystem::path ───────────────────────
namespace nlohmann { namespace nlohmann {
template <> template <>
struct adl_serializer<std::filesystem::path> { struct adl_serializer<std::filesystem::path> {
@ -24,40 +28,35 @@ struct adl_serializer<std::filesystem::path> {
}; };
} // namespace nlohmann } // namespace nlohmann
// -------------------- // ── Helpers ───────────────────────────────────────────────────────────
// Print summary
// --------------------
void EmulatorSettings::PrintChangedSummary(const std::vector<std::string>& changed) { void EmulatorSettings::PrintChangedSummary(const std::vector<std::string>& changed) {
if (changed.empty()) { if (changed.empty()) {
LOG_DEBUG(EmuSettings, "[Settings] No game-specific overrides applied"); LOG_DEBUG(EmuSettings, "No game-specific overrides applied");
return; return;
} }
LOG_DEBUG(EmuSettings, "[Settings] Game-specific overrides applied:"); LOG_DEBUG(EmuSettings, "Game-specific overrides applied:");
for (const auto& k : changed) { for (const auto& k : changed)
LOG_DEBUG(EmuSettings, " * {}", k); LOG_DEBUG(EmuSettings, " * {}", k);
}
} }
// -------------------- // ── Singleton ────────────────────────────────────────────────────────
// ctor/dtor + singleton EmulatorSettings::EmulatorSettings() = default;
// --------------------
EmulatorSettings::EmulatorSettings() {
// Load();
}
EmulatorSettings::~EmulatorSettings() { EmulatorSettings::~EmulatorSettings() {
Save(); Save();
} }
std::shared_ptr<EmulatorSettings> EmulatorSettings::GetInstance() { std::shared_ptr<EmulatorSettings> EmulatorSettings::GetInstance() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard lock(s_mutex);
if (!s_instance) if (!s_instance)
s_instance = std::make_shared<EmulatorSettings>(); s_instance = std::make_shared<EmulatorSettings>();
return s_instance; return s_instance;
} }
void EmulatorSettings::SetInstance(std::shared_ptr<EmulatorSettings> instance) { void EmulatorSettings::SetInstance(std::shared_ptr<EmulatorSettings> instance) {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard lock(s_mutex);
s_instance = instance; s_instance = std::move(instance);
} }
// -------------------- // --------------------
@ -153,88 +152,89 @@ void EmulatorSettings::SetFontsDir(const std::filesystem::path& dir) {
m_general.font_dir.value = dir; m_general.font_dir.value = dir;
} }
// -------------------- // ── Game-specific override management ────────────────────────────────
// Save void EmulatorSettings::ClearGameSpecificOverrides() {
// -------------------- ClearGroupOverrides(m_general);
ClearGroupOverrides(m_debug);
ClearGroupOverrides(m_input);
ClearGroupOverrides(m_audio);
ClearGroupOverrides(m_gpu);
ClearGroupOverrides(m_vulkan);
LOG_DEBUG(EmuSettings, "All game-specific overrides cleared");
}
void EmulatorSettings::ResetGameSpecificValue(const std::string& key) {
// Walk every overrideable group until we find the matching key.
auto tryGroup = [&key](auto& group) {
for (auto& item : group.GetOverrideableFields()) {
if (key == item.key) {
item.reset_game_specific(&group);
return true;
}
}
return false;
};
if (tryGroup(m_general))
return;
if (tryGroup(m_debug))
return;
if (tryGroup(m_input))
return;
if (tryGroup(m_audio))
return;
if (tryGroup(m_gpu))
return;
if (tryGroup(m_vulkan))
return;
LOG_WARNING(EmuSettings, "ResetGameSpecificValue: key '{}' not found", key);
}
bool EmulatorSettings::Save(const std::string& serial) const { bool EmulatorSettings::Save(const std::string& serial) const {
try { try {
if (!serial.empty()) { if (!serial.empty()) {
const std::filesystem::path cfgDir = const auto cfgDir = Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs);
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs);
std::filesystem::create_directories(cfgDir); std::filesystem::create_directories(cfgDir);
const std::filesystem::path path = cfgDir / (serial + ".json"); const auto path = cfgDir / (serial + ".json");
json j = json::object(); json j = json::object();
// Only write overrideable fields for each group
json generalObj = json::object(); json generalObj = json::object();
for (auto& item : m_general.GetOverrideableFields()) { SaveGroupGameSpecific(m_general, generalObj);
json whole = m_general;
if (whole.contains(item.key))
generalObj[item.key] = whole[item.key];
}
j["General"] = generalObj; j["General"] = generalObj;
// Debug
json debugObj = json::object(); json debugObj = json::object();
for (auto& item : m_debug.GetOverrideableFields()) { SaveGroupGameSpecific(m_debug, debugObj);
json whole = m_debug;
if (whole.contains(item.key))
debugObj[item.key] = whole[item.key];
}
j["Debug"] = debugObj; j["Debug"] = debugObj;
// Input
json inputObj = json::object(); json inputObj = json::object();
for (auto& item : m_input.GetOverrideableFields()) { SaveGroupGameSpecific(m_input, inputObj);
json whole = m_input;
if (whole.contains(item.key))
inputObj[item.key] = whole[item.key];
}
j["Input"] = inputObj; j["Input"] = inputObj;
// Audio
json audioObj = json::object(); json audioObj = json::object();
for (auto& item : m_audio.GetOverrideableFields()) { SaveGroupGameSpecific(m_audio, audioObj);
json whole = m_audio;
if (whole.contains(item.key))
audioObj[item.key] = whole[item.key];
}
j["Audio"] = audioObj; j["Audio"] = audioObj;
// GPU
json gpuObj = json::object(); json gpuObj = json::object();
for (auto& item : m_gpu.GetOverrideableFields()) { SaveGroupGameSpecific(m_gpu, gpuObj);
json whole = m_gpu;
if (whole.contains(item.key))
gpuObj[item.key] = whole[item.key];
}
j["GPU"] = gpuObj; j["GPU"] = gpuObj;
// Vulkan
json vulkanObj = json::object(); json vulkanObj = json::object();
for (auto& item : m_vulkan.GetOverrideableFields()) { SaveGroupGameSpecific(m_vulkan, vulkanObj);
json whole = m_vulkan;
if (whole.contains(item.key))
vulkanObj[item.key] = whole[item.key];
}
j["Vulkan"] = vulkanObj; j["Vulkan"] = vulkanObj;
std::ofstream out(path); std::ofstream out(path);
if (!out.is_open()) { if (!out) {
LOG_ERROR(EmuSettings, "Failed to open file for writing: {}", path.string()); LOG_ERROR(EmuSettings, "Failed to open game config for writing: {}", path.string());
return false; return false;
} }
out << std::setw(4) << j; out << std::setw(4) << j;
out.flush(); return !out.fail();
if (out.fail()) {
LOG_ERROR(EmuSettings, "Failed to write settings to: {}", path.string());
return false;
}
return true;
} else { } else {
const std::filesystem::path path = // ── Global config.json ─────────────────────────────────────
const auto path =
Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json"; Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json";
json j; json j;
j["General"] = m_general; j["General"] = m_general;
j["Debug"] = m_debug; j["Debug"] = m_debug;
@ -243,19 +243,13 @@ bool EmulatorSettings::Save(const std::string& serial) const {
j["GPU"] = m_gpu; j["GPU"] = m_gpu;
j["Vulkan"] = m_vulkan; j["Vulkan"] = m_vulkan;
j["Users"] = m_userManager.GetUsers(); j["Users"] = m_userManager.GetUsers();
std::ofstream out(path); std::ofstream out(path);
if (!out.is_open()) { if (!out) {
LOG_ERROR(EmuSettings, "Failed to open file for writing: {}", path.string()); LOG_ERROR(EmuSettings, "Failed to open config for writing: {}", path.string());
return false; return false;
} }
out << std::setw(4) << j; out << std::setw(4) << j;
out.flush(); return !out.fail();
if (out.fail()) {
LOG_ERROR(EmuSettings, "Failed to write settings to: {}", path.string());
return false;
}
return true;
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
LOG_ERROR(EmuSettings, "Error saving settings: {}", e.what()); LOG_ERROR(EmuSettings, "Error saving settings: {}", e.what());
@ -263,146 +257,90 @@ bool EmulatorSettings::Save(const std::string& serial) const {
} }
} }
// -------------------- // ── Load ──────────────────────────────────────────────────────────────
// Load
// --------------------
bool EmulatorSettings::Load(const std::string& serial) { bool EmulatorSettings::Load(const std::string& serial) {
try { try {
// If serial is empty, load ONLY global settings
if (serial.empty()) { if (serial.empty()) {
const std::filesystem::path userDir = // ── Global config ──────────────────────────────────────────
Common::FS::GetUserPath(Common::FS::PathType::UserDir); const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const std::filesystem::path configPath = userDir / "config.json"; const auto configPath = userDir / "config.json";
LOG_DEBUG(EmuSettings, "Loading global config from: {}", configPath.string());
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loading global settings from: {}", if (std::ifstream in{configPath}; in.good()) {
configPath.string());
// Load global config if exists
if (std::ifstream globalIn{configPath}; globalIn.good()) {
json gj; json gj;
globalIn >> gj; in >> gj;
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Global config JSON size: {}", gj.size()); auto mergeGroup = [&gj](auto& group, const char* section) {
if (!gj.contains(section))
return;
json current = group;
current.update(gj.at(section));
group = current.get<std::remove_reference_t<decltype(group)>>();
};
if (gj.contains("General")) { mergeGroup(m_general, "General");
json current = m_general; mergeGroup(m_debug, "Debug");
current.update(gj.at("General")); mergeGroup(m_input, "Input");
m_general = current.get<GeneralSettings>(); mergeGroup(m_audio, "Audio");
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded General settings"); mergeGroup(m_gpu, "GPU");
} mergeGroup(m_vulkan, "Vulkan");
if (gj.contains("Debug")) {
json current = m_debug; if (gj.contains("Users"))
current.update(gj.at("Debug"));
m_debug = current.get<DebugSettings>();
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Debug settings");
}
if (gj.contains("Input")) {
json current = m_input;
current.update(gj.at("Input"));
m_input = current.get<InputSettings>();
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Input settings");
}
if (gj.contains("Audio")) {
json current = m_audio;
current.update(gj.at("Audio"));
m_audio = current.get<AudioSettings>();
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Audio settings");
}
if (gj.contains("GPU")) {
json current = m_gpu;
current.update(gj.at("GPU"));
m_gpu = current.get<GPUSettings>();
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded GPU settings");
}
if (gj.contains("Vulkan")) {
json current = m_vulkan;
current.update(gj.at("Vulkan"));
m_vulkan = current.get<VulkanSettings>();
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Vulkan settings");
}
if (gj.contains("Users")) {
m_userManager.GetUsers() = gj.at("Users").get<Users>(); m_userManager.GetUsers() = gj.at("Users").get<Users>();
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Users"); LOG_DEBUG(EmuSettings, "Global config loaded successfully");
}
} else { } else {
LOG_DEBUG(EmuSettings, LOG_DEBUG(EmuSettings, "Global config not found using defaults");
"[EmulatorSettings] Global config not found, setting defaults");
SetDefaultValues(); SetDefaultValues();
// ensure a default user exists
if (m_userManager.GetUsers().user.empty()) if (m_userManager.GetUsers().user.empty())
m_userManager.GetUsers().user = m_userManager.CreateDefaultUser(); m_userManager.GetUsers().user = m_userManager.CreateDefaultUser();
Save(); Save();
} }
return true; return true;
}
// If serial is provided, ONLY apply game-specific overrides
// WITHOUT reloading global settings!
else {
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying game-specific overrides for: {}",
serial);
const std::filesystem::path gamePath = } else {
// ── Per-game override file ─────────────────────────────────
// Never reloads global settings. Only applies
// game_specific_value overrides on top of the already-loaded
// base configuration.
const auto gamePath =
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json"); Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json");
LOG_DEBUG(EmuSettings, "Applying game config: {}", gamePath.string());
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Game config path: {}", gamePath.string());
if (!std::filesystem::exists(gamePath)) { if (!std::filesystem::exists(gamePath)) {
LOG_DEBUG(EmuSettings, "[EmulatorSettings] No game-specific config found"); LOG_DEBUG(EmuSettings, "No game-specific config found for {}", serial);
return false; return false;
} }
std::ifstream in(gamePath); std::ifstream in(gamePath);
if (!in.is_open()) { if (!in) {
LOG_ERROR(EmuSettings, "[EmulatorSettings] Failed to open game config file"); LOG_ERROR(EmuSettings, "Failed to open game config: {}", gamePath.string());
return false; return false;
} }
json gj; json gj;
in >> gj; in >> gj;
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Game config JSON: {}", gj.dump(2));
std::vector<std::string> changed; std::vector<std::string> changed;
if (gj.contains("General")) { // ApplyGroupOverrides now correctly stores values as
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying General overrides"); // game_specific_value (see make_override in the header).
ApplyGroupOverrides<GeneralSettings>(m_general, gj.at("General"), changed); // ConfigMode::Default will then resolve them at getter call
} // time without ever touching the base values.
if (gj.contains("Debug")) { if (gj.contains("General"))
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Debug overrides"); ApplyGroupOverrides(m_general, gj.at("General"), changed);
ApplyGroupOverrides<DebugSettings>(m_debug, gj.at("Debug"), changed); if (gj.contains("Debug"))
} ApplyGroupOverrides(m_debug, gj.at("Debug"), changed);
if (gj.contains("Input")) { if (gj.contains("Input"))
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Input overrides"); ApplyGroupOverrides(m_input, gj.at("Input"), changed);
ApplyGroupOverrides<InputSettings>(m_input, gj.at("Input"), changed); if (gj.contains("Audio"))
} ApplyGroupOverrides(m_audio, gj.at("Audio"), changed);
if (gj.contains("Audio")) { if (gj.contains("GPU"))
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Audio overrides"); ApplyGroupOverrides(m_gpu, gj.at("GPU"), changed);
ApplyGroupOverrides<AudioSettings>(m_audio, gj.at("Audio"), changed); if (gj.contains("Vulkan"))
} ApplyGroupOverrides(m_vulkan, gj.at("Vulkan"), changed);
if (gj.contains("GPU")) {
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying GPU overrides");
ApplyGroupOverrides<GPUSettings>(m_gpu, gj.at("GPU"), changed);
// Debug: Print specific GPU values
auto gpuJson = gj["GPU"];
if (gpuJson.contains("fsr_enabled")) {
LOG_DEBUG(EmuSettings, "[EmulatorSettings] GPU/fsr_enabled JSON: {}",
gpuJson["fsr_enabled"].dump());
}
if (gpuJson.contains("rcas_enabled")) {
LOG_DEBUG(EmuSettings, "[EmulatorSettings] GPU/rcas_enabled JSON: {}",
gpuJson["rcas_enabled"].dump());
}
}
if (gj.contains("Vulkan")) {
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Vulkan overrides");
ApplyGroupOverrides<VulkanSettings>(m_vulkan, gj.at("Vulkan"), changed);
}
PrintChangedSummary(changed); PrintChangedSummary(changed);
return true; return true;
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
@ -422,19 +360,15 @@ void EmulatorSettings::SetDefaultValues() {
std::vector<std::string> EmulatorSettings::GetAllOverrideableKeys() const { std::vector<std::string> EmulatorSettings::GetAllOverrideableKeys() const {
std::vector<std::string> keys; std::vector<std::string> keys;
auto addGroup = [&keys](const auto& fields) {
auto addKeys = [&keys](const std::vector<OverrideItem>& items) { for (const auto& item : fields)
for (const auto& item : items) {
keys.push_back(item.key); keys.push_back(item.key);
}
}; };
addGroup(m_general.GetOverrideableFields());
addKeys(m_general.GetOverrideableFields()); addGroup(m_debug.GetOverrideableFields());
addKeys(m_debug.GetOverrideableFields()); addGroup(m_input.GetOverrideableFields());
addKeys(m_input.GetOverrideableFields()); addGroup(m_audio.GetOverrideableFields());
addKeys(m_audio.GetOverrideableFields()); addGroup(m_gpu.GetOverrideableFields());
addKeys(m_gpu.GetOverrideableFields()); addGroup(m_vulkan.GetOverrideableFields());
addKeys(m_vulkan.GetOverrideableFields());
return keys; return keys;
} }

View File

@ -15,12 +15,47 @@
#include "common/types.h" #include "common/types.h"
#include "core/user_manager.h" #include "core/user_manager.h"
// ------------------------------- enum class ConfigMode {
// Generic Setting wrapper Default,
// ------------------------------- Global,
Clean,
};
template <typename T> template <typename T>
struct Setting { struct Setting {
T default_value{};
T value{}; T value{};
std::optional<T> game_specific_value{};
Setting() = default;
// Single-argument ctor: initialises both default_value and value so
// that CleanMode can always recover the intended factory default.
/*implicit*/ Setting(T init) : default_value(std::move(init)), value(default_value) {}
/// Return the active value under the given mode.
T get(ConfigMode mode = ConfigMode::Default) const {
switch (mode) {
case ConfigMode::Default:
return game_specific_value.value_or(value);
case ConfigMode::Global:
return value;
case ConfigMode::Clean:
return default_value;
}
return value;
}
/// Write v to the base layer.
/// Game-specific overrides are applied exclusively via Load(serial)
void set(const T& v) {
value = v;
}
/// Discard the game-specific override; subsequent get(Default) will
/// fall back to the base value.
void reset_game_specific() {
game_specific_value = std::nullopt;
}
}; };
template <typename T> template <typename T>
@ -33,16 +68,19 @@ void from_json(const nlohmann::json& j, Setting<T>& s) {
s.value = j.get<T>(); s.value = j.get<T>();
} }
// -------------------------------
// Helper to describe a per-field override action
// -------------------------------
struct OverrideItem { struct OverrideItem {
const char* key; const char* key;
// apply(basePtrToStruct, jsonEntry, changedFields) std::function<void(void* group_ptr, const nlohmann::json& entry,
std::function<void(void*, const nlohmann::json&, std::vector<std::string>&)> apply; std::vector<std::string>& changed)>
apply;
/// Return the value that should be written to the per-game config file.
/// Falls back to base value if no game-specific override is set.
std::function<nlohmann::json(const void* group_ptr)> get_for_save;
/// Clear game_specific_value for this field.
std::function<void(void* group_ptr)> reset_game_specific;
}; };
// Helper factory: create an OverrideItem binding a pointer-to-member
template <typename Struct, typename T> template <typename Struct, typename T>
inline OverrideItem make_override(const char* key, Setting<T> Struct::* member) { inline OverrideItem make_override(const char* key, Setting<T> Struct::* member) {
return OverrideItem{ return OverrideItem{
@ -50,31 +88,41 @@ inline OverrideItem make_override(const char* key, Setting<T> Struct::* member)
[member, key](void* base, const nlohmann::json& entry, std::vector<std::string>& changed) { [member, key](void* base, const nlohmann::json& entry, std::vector<std::string>& changed) {
LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key); LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key);
LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump()); LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump());
Struct* obj = reinterpret_cast<Struct*>(base); Struct* obj = reinterpret_cast<Struct*>(base);
Setting<T>& dst = obj->*member; Setting<T>& dst = obj->*member;
try { try {
// Parse the value from JSON
T newValue = entry.get<T>(); T newValue = entry.get<T>();
LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue); LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue);
LOG_DEBUG(EmuSettings, "[make_override] Current value: {}", dst.value); LOG_DEBUG(EmuSettings, "[make_override] Current value: {}", dst.value);
if (dst.value != newValue) { if (dst.value != newValue) {
std::ostringstream oss; std::ostringstream oss;
oss << key << " ( " << dst.value << "" << newValue << " )"; oss << key << " ( " << dst.value << "" << newValue << " )";
changed.push_back(oss.str()); changed.push_back(oss.str());
LOG_DEBUG(EmuSettings, "[make_override] Recorded change: {}", oss.str()); LOG_DEBUG(EmuSettings, "[make_override] Recorded change: {}", oss.str());
} }
dst.game_specific_value = newValue;
dst.value = newValue;
LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key); LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key);
} catch (const std::exception& e) { } catch (const std::exception& e) {
LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what()); LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what());
LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump()); LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump());
LOG_ERROR(EmuSettings, "[make_override] Type name: {}", entry.type_name()); LOG_ERROR(EmuSettings, "[make_override] Type name: {}", entry.type_name());
} }
},
// --- get_for_save -------------------------------------------
// Returns game_specific_value when present, otherwise base value.
// This means a freshly-opened game-specific dialog still shows
// useful (current-global) values rather than empty entries.
[member](const void* base) -> nlohmann::json {
const Struct* obj = reinterpret_cast<const Struct*>(base);
const Setting<T>& src = obj->*member;
return nlohmann::json(src.game_specific_value.value_or(src.value));
},
// --- reset_game_specific ------------------------------------
[member](void* base) {
Struct* obj = reinterpret_cast<Struct*>(base);
(obj->*member).reset_game_specific();
}}; }};
} }
@ -339,6 +387,23 @@ public:
bool Load(const std::string& serial = ""); bool Load(const std::string& serial = "");
void SetDefaultValues(); void SetDefaultValues();
// Config mode
ConfigMode GetConfigMode() const {
return m_configMode;
}
void SetConfigMode(ConfigMode mode) {
m_configMode = mode;
}
//
// Game-specific override management
/// Clears all per-game overrides. Call this when a game exits so
/// the emulator reverts to global settings.
void ClearGameSpecificOverrides();
/// Reset a single field's game-specific override by its JSON ke
void ResetGameSpecificValue(const std::string& key);
// general accessors // general accessors
bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true); bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true);
std::vector<std::filesystem::path> GetGameInstallDirs() const; std::vector<std::filesystem::path> GetGameInstallDirs() const;
@ -369,11 +434,12 @@ private:
GPUSettings m_gpu{}; GPUSettings m_gpu{};
VulkanSettings m_vulkan{}; VulkanSettings m_vulkan{};
UserManager m_userManager; UserManager m_userManager;
ConfigMode m_configMode{ConfigMode::Default};
static std::shared_ptr<EmulatorSettings> s_instance; static std::shared_ptr<EmulatorSettings> s_instance;
static std::mutex s_mutex; static std::mutex s_mutex;
// Generic helper that applies override descriptors for a specific group /// Apply overrideable fields from groupJson into group.game_specific_value.
template <typename Group> template <typename Group>
void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson,
std::vector<std::string>& changed) { std::vector<std::string>& changed) {
@ -384,6 +450,20 @@ private:
} }
} }
// Write all overrideable fields from group into out (for game-specific save).
template <typename Group>
static void SaveGroupGameSpecific(const Group& group, nlohmann::json& out) {
for (auto& item : group.GetOverrideableFields())
out[item.key] = item.get_for_save(&group);
}
// Discard every game-specific override in group.
template <typename Group>
static void ClearGroupOverrides(Group& group) {
for (auto& item : group.GetOverrideableFields())
item.reset_game_specific(&group);
}
static void PrintChangedSummary(const std::vector<std::string>& changed); static void PrintChangedSummary(const std::vector<std::string>& changed);
public: public:
@ -407,23 +487,24 @@ public:
return m_vulkan.GetOverrideableFields(); return m_vulkan.GetOverrideableFields();
} }
std::vector<std::string> GetAllOverrideableKeys() const; std::vector<std::string> GetAllOverrideableKeys() const;
#define SETTING_FORWARD(group, Name, field) \ #define SETTING_FORWARD(group, Name, field) \
auto Get##Name() const { \ auto Get##Name() const { \
return group.field.value; \ return (group).field.get(m_configMode); \
} \ } \
void Set##Name(const decltype(group.field.value)& v) { \ void Set##Name(const decltype((group).field.value)& v) { \
group.field.value = v; \ (group).field.value = v; \
} }
#define SETTING_FORWARD_BOOL(group, Name, field) \ #define SETTING_FORWARD_BOOL(group, Name, field) \
auto Is##Name() const { \ bool Is##Name() const { \
return group.field.value; \ return (group).field.get(m_configMode); \
} \ } \
void Set##Name(const decltype(group.field.value)& v) { \ void Set##Name(bool v) { \
group.field.value = v; \ (group).field.value = v; \
} }
#define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ #define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \
auto Is##Name() const { \ bool Is##Name() const { \
return group.field.value; \ return (group).field.get(m_configMode); \
} }
// General settings // General settings
@ -516,4 +597,5 @@ public:
#undef SETTING_FORWARD #undef SETTING_FORWARD
#undef SETTING_FORWARD_BOOL #undef SETTING_FORWARD_BOOL
#undef SETTING_FORWARD_BOOL_READONLY
}; };