diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index f4717e4be..a3f63d0e2 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -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 +#include #include #include +#include #include #include "common/logging/log.h" #include "emulator_settings.h" using json = nlohmann::json; +// ── Singleton storage ───────────────────────────────────────────────── std::shared_ptr EmulatorSettings::s_instance = nullptr; std::mutex EmulatorSettings::s_mutex; +// ── nlohmann helpers for std::filesystem::path ─────────────────────── namespace nlohmann { template <> struct adl_serializer { @@ -24,40 +28,35 @@ struct adl_serializer { }; } // namespace nlohmann -// -------------------- -// Print summary -// -------------------- +// ── Helpers ─────────────────────────────────────────────────────────── + void EmulatorSettings::PrintChangedSummary(const std::vector& changed) { if (changed.empty()) { - LOG_DEBUG(EmuSettings, "[Settings] No game-specific overrides applied"); + LOG_DEBUG(EmuSettings, "No game-specific overrides applied"); return; } - LOG_DEBUG(EmuSettings, "[Settings] Game-specific overrides applied:"); - for (const auto& k : changed) { + LOG_DEBUG(EmuSettings, "Game-specific overrides applied:"); + for (const auto& k : changed) LOG_DEBUG(EmuSettings, " * {}", k); - } } -// -------------------- -// ctor/dtor + singleton -// -------------------- -EmulatorSettings::EmulatorSettings() { - // Load(); -} +// ── Singleton ──────────────────────────────────────────────────────── +EmulatorSettings::EmulatorSettings() = default; + EmulatorSettings::~EmulatorSettings() { Save(); } std::shared_ptr EmulatorSettings::GetInstance() { - std::lock_guard lock(s_mutex); + std::lock_guard lock(s_mutex); if (!s_instance) s_instance = std::make_shared(); return s_instance; } void EmulatorSettings::SetInstance(std::shared_ptr instance) { - std::lock_guard lock(s_mutex); - s_instance = instance; + std::lock_guard lock(s_mutex); + s_instance = std::move(instance); } // -------------------- @@ -153,88 +152,89 @@ void EmulatorSettings::SetFontsDir(const std::filesystem::path& dir) { m_general.font_dir.value = dir; } -// -------------------- -// Save -// -------------------- +// ── Game-specific override management ──────────────────────────────── +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 { try { if (!serial.empty()) { - const std::filesystem::path cfgDir = - Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs); + const auto cfgDir = Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs); std::filesystem::create_directories(cfgDir); - const std::filesystem::path path = cfgDir / (serial + ".json"); + const auto path = cfgDir / (serial + ".json"); json j = json::object(); - // Only write overrideable fields for each group json generalObj = json::object(); - for (auto& item : m_general.GetOverrideableFields()) { - json whole = m_general; - if (whole.contains(item.key)) - generalObj[item.key] = whole[item.key]; - } + SaveGroupGameSpecific(m_general, generalObj); j["General"] = generalObj; - // Debug json debugObj = json::object(); - for (auto& item : m_debug.GetOverrideableFields()) { - json whole = m_debug; - if (whole.contains(item.key)) - debugObj[item.key] = whole[item.key]; - } + SaveGroupGameSpecific(m_debug, debugObj); j["Debug"] = debugObj; - // Input json inputObj = json::object(); - for (auto& item : m_input.GetOverrideableFields()) { - json whole = m_input; - if (whole.contains(item.key)) - inputObj[item.key] = whole[item.key]; - } + SaveGroupGameSpecific(m_input, inputObj); j["Input"] = inputObj; - // Audio json audioObj = json::object(); - for (auto& item : m_audio.GetOverrideableFields()) { - json whole = m_audio; - if (whole.contains(item.key)) - audioObj[item.key] = whole[item.key]; - } + SaveGroupGameSpecific(m_audio, audioObj); j["Audio"] = audioObj; - // GPU json gpuObj = json::object(); - for (auto& item : m_gpu.GetOverrideableFields()) { - json whole = m_gpu; - if (whole.contains(item.key)) - gpuObj[item.key] = whole[item.key]; - } + SaveGroupGameSpecific(m_gpu, gpuObj); j["GPU"] = gpuObj; - // Vulkan json vulkanObj = json::object(); - for (auto& item : m_vulkan.GetOverrideableFields()) { - json whole = m_vulkan; - if (whole.contains(item.key)) - vulkanObj[item.key] = whole[item.key]; - } + SaveGroupGameSpecific(m_vulkan, vulkanObj); j["Vulkan"] = vulkanObj; std::ofstream out(path); - if (!out.is_open()) { - LOG_ERROR(EmuSettings, "Failed to open file for writing: {}", path.string()); + if (!out) { + LOG_ERROR(EmuSettings, "Failed to open game config for writing: {}", path.string()); return false; } out << std::setw(4) << j; - out.flush(); - if (out.fail()) { - LOG_ERROR(EmuSettings, "Failed to write settings to: {}", path.string()); - return false; - } - return true; + return !out.fail(); + } else { - const std::filesystem::path path = + // ── Global config.json ───────────────────────────────────── + const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json"; + json j; j["General"] = m_general; j["Debug"] = m_debug; @@ -243,19 +243,13 @@ bool EmulatorSettings::Save(const std::string& serial) const { j["GPU"] = m_gpu; j["Vulkan"] = m_vulkan; j["Users"] = m_userManager.GetUsers(); - std::ofstream out(path); - if (!out.is_open()) { - LOG_ERROR(EmuSettings, "Failed to open file for writing: {}", path.string()); + if (!out) { + LOG_ERROR(EmuSettings, "Failed to open config for writing: {}", path.string()); return false; } out << std::setw(4) << j; - out.flush(); - if (out.fail()) { - LOG_ERROR(EmuSettings, "Failed to write settings to: {}", path.string()); - return false; - } - return true; + return !out.fail(); } } catch (const std::exception& e) { 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) { try { - // If serial is empty, load ONLY global settings if (serial.empty()) { - const std::filesystem::path userDir = - Common::FS::GetUserPath(Common::FS::PathType::UserDir); - const std::filesystem::path configPath = userDir / "config.json"; + // ── Global config ────────────────────────────────────────── + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto configPath = userDir / "config.json"; + LOG_DEBUG(EmuSettings, "Loading global config from: {}", configPath.string()); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loading global settings from: {}", - configPath.string()); - - // Load global config if exists - if (std::ifstream globalIn{configPath}; globalIn.good()) { + if (std::ifstream in{configPath}; in.good()) { 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>(); + }; - if (gj.contains("General")) { - json current = m_general; - current.update(gj.at("General")); - m_general = current.get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded General settings"); - } - if (gj.contains("Debug")) { - json current = m_debug; - current.update(gj.at("Debug")); - m_debug = current.get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Debug settings"); - } - if (gj.contains("Input")) { - json current = m_input; - current.update(gj.at("Input")); - m_input = current.get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Input settings"); - } - if (gj.contains("Audio")) { - json current = m_audio; - current.update(gj.at("Audio")); - m_audio = current.get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Audio settings"); - } - if (gj.contains("GPU")) { - json current = m_gpu; - current.update(gj.at("GPU")); - m_gpu = current.get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded GPU settings"); - } - if (gj.contains("Vulkan")) { - json current = m_vulkan; - current.update(gj.at("Vulkan")); - m_vulkan = current.get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Vulkan settings"); - } - if (gj.contains("Users")) { + mergeGroup(m_general, "General"); + mergeGroup(m_debug, "Debug"); + mergeGroup(m_input, "Input"); + mergeGroup(m_audio, "Audio"); + mergeGroup(m_gpu, "GPU"); + mergeGroup(m_vulkan, "Vulkan"); + + if (gj.contains("Users")) m_userManager.GetUsers() = gj.at("Users").get(); - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded Users"); - } + LOG_DEBUG(EmuSettings, "Global config loaded successfully"); } else { - LOG_DEBUG(EmuSettings, - "[EmulatorSettings] Global config not found, setting defaults"); + LOG_DEBUG(EmuSettings, "Global config not found – using defaults"); SetDefaultValues(); - // ensure a default user exists if (m_userManager.GetUsers().user.empty()) m_userManager.GetUsers().user = m_userManager.CreateDefaultUser(); Save(); } - 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"); - - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Game config path: {}", gamePath.string()); + LOG_DEBUG(EmuSettings, "Applying game config: {}", gamePath.string()); 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; } std::ifstream in(gamePath); - if (!in.is_open()) { - LOG_ERROR(EmuSettings, "[EmulatorSettings] Failed to open game config file"); + if (!in) { + LOG_ERROR(EmuSettings, "Failed to open game config: {}", gamePath.string()); return false; } json gj; in >> gj; - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Game config JSON: {}", gj.dump(2)); - std::vector changed; - if (gj.contains("General")) { - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying General overrides"); - ApplyGroupOverrides(m_general, gj.at("General"), changed); - } - if (gj.contains("Debug")) { - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Debug overrides"); - ApplyGroupOverrides(m_debug, gj.at("Debug"), changed); - } - if (gj.contains("Input")) { - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Input overrides"); - ApplyGroupOverrides(m_input, gj.at("Input"), changed); - } - if (gj.contains("Audio")) { - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Audio overrides"); - ApplyGroupOverrides(m_audio, gj.at("Audio"), changed); - } - if (gj.contains("GPU")) { - LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying GPU overrides"); - ApplyGroupOverrides(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(m_vulkan, gj.at("Vulkan"), changed); - } + // ApplyGroupOverrides now correctly stores values as + // game_specific_value (see make_override in the header). + // ConfigMode::Default will then resolve them at getter call + // time without ever touching the base values. + if (gj.contains("General")) + ApplyGroupOverrides(m_general, gj.at("General"), changed); + if (gj.contains("Debug")) + ApplyGroupOverrides(m_debug, gj.at("Debug"), changed); + if (gj.contains("Input")) + ApplyGroupOverrides(m_input, gj.at("Input"), changed); + if (gj.contains("Audio")) + ApplyGroupOverrides(m_audio, gj.at("Audio"), changed); + if (gj.contains("GPU")) + ApplyGroupOverrides(m_gpu, gj.at("GPU"), changed); + if (gj.contains("Vulkan")) + ApplyGroupOverrides(m_vulkan, gj.at("Vulkan"), changed); PrintChangedSummary(changed); - return true; } } catch (const std::exception& e) { @@ -422,19 +360,15 @@ void EmulatorSettings::SetDefaultValues() { std::vector EmulatorSettings::GetAllOverrideableKeys() const { std::vector keys; - - auto addKeys = [&keys](const std::vector& items) { - for (const auto& item : items) { + auto addGroup = [&keys](const auto& fields) { + for (const auto& item : fields) keys.push_back(item.key); - } }; - - addKeys(m_general.GetOverrideableFields()); - addKeys(m_debug.GetOverrideableFields()); - addKeys(m_input.GetOverrideableFields()); - addKeys(m_audio.GetOverrideableFields()); - addKeys(m_gpu.GetOverrideableFields()); - addKeys(m_vulkan.GetOverrideableFields()); - + addGroup(m_general.GetOverrideableFields()); + addGroup(m_debug.GetOverrideableFields()); + addGroup(m_input.GetOverrideableFields()); + addGroup(m_audio.GetOverrideableFields()); + addGroup(m_gpu.GetOverrideableFields()); + addGroup(m_vulkan.GetOverrideableFields()); return keys; } \ No newline at end of file diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index 13e1b7687..976e18c8b 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -15,12 +15,47 @@ #include "common/types.h" #include "core/user_manager.h" -// ------------------------------- -// Generic Setting wrapper -// ------------------------------- +enum class ConfigMode { + Default, + Global, + Clean, +}; + template struct Setting { + T default_value{}; T value{}; + std::optional 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 @@ -33,16 +68,19 @@ void from_json(const nlohmann::json& j, Setting& s) { s.value = j.get(); } -// ------------------------------- -// Helper to describe a per-field override action -// ------------------------------- struct OverrideItem { const char* key; - // apply(basePtrToStruct, jsonEntry, changedFields) - std::function&)> apply; + std::function& 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 get_for_save; + + /// Clear game_specific_value for this field. + std::function reset_game_specific; }; -// Helper factory: create an OverrideItem binding a pointer-to-member template inline OverrideItem make_override(const char* key, Setting Struct::* member) { return OverrideItem{ @@ -50,31 +88,41 @@ inline OverrideItem make_override(const char* key, Setting Struct::* member) [member, key](void* base, const nlohmann::json& entry, std::vector& changed) { LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key); LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump()); - Struct* obj = reinterpret_cast(base); Setting& dst = obj->*member; - try { - // Parse the value from JSON T newValue = entry.get(); - LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue); LOG_DEBUG(EmuSettings, "[make_override] Current value: {}", dst.value); - if (dst.value != newValue) { std::ostringstream oss; oss << key << " ( " << dst.value << " → " << newValue << " )"; changed.push_back(oss.str()); LOG_DEBUG(EmuSettings, "[make_override] Recorded change: {}", oss.str()); } - - dst.value = newValue; + dst.game_specific_value = newValue; LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key); } catch (const std::exception& e) { LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what()); LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump()); 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(base); + const Setting& src = obj->*member; + return nlohmann::json(src.game_specific_value.value_or(src.value)); + }, + + // --- reset_game_specific ------------------------------------ + [member](void* base) { + Struct* obj = reinterpret_cast(base); + (obj->*member).reset_game_specific(); }}; } @@ -339,6 +387,23 @@ public: bool Load(const std::string& serial = ""); 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 bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true); std::vector GetGameInstallDirs() const; @@ -369,11 +434,12 @@ private: GPUSettings m_gpu{}; VulkanSettings m_vulkan{}; UserManager m_userManager; + ConfigMode m_configMode{ConfigMode::Default}; static std::shared_ptr s_instance; 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 void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, std::vector& changed) { @@ -384,6 +450,20 @@ private: } } + // Write all overrideable fields from group into out (for game-specific save). + template + 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 + static void ClearGroupOverrides(Group& group) { + for (auto& item : group.GetOverrideableFields()) + item.reset_game_specific(&group); + } + static void PrintChangedSummary(const std::vector& changed); public: @@ -407,23 +487,24 @@ public: return m_vulkan.GetOverrideableFields(); } std::vector GetAllOverrideableKeys() const; + #define SETTING_FORWARD(group, Name, field) \ auto Get##Name() const { \ - return group.field.value; \ + return (group).field.get(m_configMode); \ } \ - void Set##Name(const decltype(group.field.value)& v) { \ - group.field.value = v; \ + void Set##Name(const decltype((group).field.value)& v) { \ + (group).field.value = v; \ } #define SETTING_FORWARD_BOOL(group, Name, field) \ - auto Is##Name() const { \ - return group.field.value; \ + bool Is##Name() const { \ + return (group).field.get(m_configMode); \ } \ - void Set##Name(const decltype(group.field.value)& v) { \ - group.field.value = v; \ + void Set##Name(bool v) { \ + (group).field.value = v; \ } #define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ - auto Is##Name() const { \ - return group.field.value; \ + bool Is##Name() const { \ + return (group).field.get(m_configMode); \ } // General settings @@ -516,4 +597,5 @@ public: #undef SETTING_FORWARD #undef SETTING_FORWARD_BOOL +#undef SETTING_FORWARD_BOOL_READONLY }; \ No newline at end of file