diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index 2faa6a0c9..d0bf43f08 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -170,6 +170,7 @@ void EmulatorSettingsImpl::ClearGameSpecificOverrides() { } void EmulatorSettingsImpl::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) { @@ -197,89 +198,34 @@ void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) { bool EmulatorSettingsImpl::Save(const std::string& serial) { try { if (!serial.empty()) { - // ── Per-game config ───────────────────────────────────── const auto cfgDir = Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs); std::filesystem::create_directories(cfgDir); const auto path = cfgDir / (serial + ".json"); - // Read existing config to preserve unknown sections - json existing_json; - if (std::filesystem::exists(path)) { - std::ifstream existing_in(path); - if (existing_in.good()) { - existing_in >> existing_json; - } - } + json j = json::object(); - json j = existing_json.is_null() ? json::object() : existing_json; - - // Save game-specific overrides (only overrideable fields) json generalObj = json::object(); SaveGroupGameSpecific(m_general, generalObj); - - // Merge with existing General section to preserve unknown fields within it - if (j.contains("General") && j["General"].is_object()) { - for (auto& [key, value] : j["General"].items()) { - if (!generalObj.contains(key)) { - generalObj[key] = value; - } - } - } j["General"] = generalObj; json debugObj = json::object(); SaveGroupGameSpecific(m_debug, debugObj); - if (j.contains("Debug") && j["Debug"].is_object()) { - for (auto& [key, value] : j["Debug"].items()) { - if (!debugObj.contains(key)) { - debugObj[key] = value; - } - } - } j["Debug"] = debugObj; json inputObj = json::object(); SaveGroupGameSpecific(m_input, inputObj); - if (j.contains("Input") && j["Input"].is_object()) { - for (auto& [key, value] : j["Input"].items()) { - if (!inputObj.contains(key)) { - inputObj[key] = value; - } - } - } j["Input"] = inputObj; json audioObj = json::object(); SaveGroupGameSpecific(m_audio, audioObj); - if (j.contains("Audio") && j["Audio"].is_object()) { - for (auto& [key, value] : j["Audio"].items()) { - if (!audioObj.contains(key)) { - audioObj[key] = value; - } - } - } j["Audio"] = audioObj; json gpuObj = json::object(); SaveGroupGameSpecific(m_gpu, gpuObj); - if (j.contains("GPU") && j["GPU"].is_object()) { - for (auto& [key, value] : j["GPU"].items()) { - if (!gpuObj.contains(key)) { - gpuObj[key] = value; - } - } - } j["GPU"] = gpuObj; json vulkanObj = json::object(); SaveGroupGameSpecific(m_vulkan, vulkanObj); - if (j.contains("Vulkan") && j["Vulkan"].is_object()) { - for (auto& [key, value] : j["Vulkan"].items()) { - if (!vulkanObj.contains(key)) { - vulkanObj[key] = value; - } - } - } j["Vulkan"] = vulkanObj; std::ofstream out(path); @@ -295,45 +241,14 @@ bool EmulatorSettingsImpl::Save(const std::string& serial) { const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json"; - // Read existing config to preserve unknown sections - json existing_json; - if (std::filesystem::exists(path)) { - std::ifstream existing_in(path); - if (existing_in.good()) { - existing_in >> existing_json; - } - } - - // Start with unknown sections we've stored from previous loads - json j = json::object(); - - // Add all unknown sections first - for (const auto& [section_name, section_data] : m_unknown_sections) { - j[section_name] = section_data; - } - - // Update schema version SetConfigVersion(Common::g_scm_rev); - m_debug.config_schema_version.value = CURRENT_CONFIG_SCHEMA_VERSION; - - // Save known sections (this will overwrite any unknown sections with the same name) + json j; j["General"] = m_general; j["Debug"] = m_debug; j["Input"] = m_input; j["Audio"] = m_audio; j["GPU"] = m_gpu; j["Vulkan"] = m_vulkan; - - // Merge with existing JSON to preserve any sections that weren't loaded - // (this is a safety net in case we missed some sections) - if (!existing_json.is_null()) { - for (auto& [key, value] : existing_json.items()) { - if (!j.contains(key)) { - j[key] = value; - } - } - } - std::ofstream out(path); if (!out) { LOG_ERROR(EmuSettings, "Failed to open config for writing: {}", path.string()); @@ -358,43 +273,24 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { const auto configPath = userDir / "config.json"; LOG_DEBUG(EmuSettings, "Loading global config from: {}", configPath.string()); - // Clear unknown sections from previous load - m_unknown_sections.clear(); - if (std::ifstream in{configPath}; in.good()) { - json j; - in >> j; + json gj; + in >> gj; - // Check schema version - int file_schema_version = 1; - if (j.contains("Debug") && j["Debug"].contains("config_schema_version")) { - file_schema_version = j["Debug"]["config_schema_version"].get(); - } + auto mergeGroup = [&gj](auto& group, const char* section) { + if (!gj.contains(section)) + return; + json current = group; + current.update(gj.at(section)); + group = current.get>(); + }; - LOG_DEBUG(EmuSettings, "Config schema version: {} (current: {})", - file_schema_version, CURRENT_CONFIG_SCHEMA_VERSION); - - // Load known sections - if (j.contains("General")) - j["General"].get_to(m_general); - if (j.contains("Debug")) - j["Debug"].get_to(m_debug); - if (j.contains("Input")) - j["Input"].get_to(m_input); - if (j.contains("Audio")) - j["Audio"].get_to(m_audio); - if (j.contains("GPU")) - j["GPU"].get_to(m_gpu); - if (j.contains("Vulkan")) - j["Vulkan"].get_to(m_vulkan); - - // Store any unknown top-level sections - for (auto it = j.begin(); it != j.end(); ++it) { - if (!IsKnownSection(it.key())) { - LOG_DEBUG(EmuSettings, "Preserving unknown section: {}", it.key()); - m_unknown_sections[it.key()] = it.value(); - } - } + mergeGroup(m_general, "General"); + mergeGroup(m_debug, "Debug"); + mergeGroup(m_input, "Input"); + mergeGroup(m_audio, "Audio"); + mergeGroup(m_gpu, "GPU"); + mergeGroup(m_vulkan, "Vulkan"); LOG_DEBUG(EmuSettings, "Global config loaded successfully"); } else { @@ -418,8 +314,8 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { int result; SDL_ShowMessageBox(&msg_box, &result); if (result == 1) { - SDL_ShowSimpleMessageBox(0, "Error", "Migration not implemented yet", - nullptr); + SDL_ShowSimpleMessageBox( + 0, "Error", "sike this actually isn't implemented yet lol", nullptr); std::quick_exit(1); } } @@ -427,17 +323,15 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { SetDefaultValues(); Save(); } - - // Update schema version if needed - if (m_debug.config_schema_version.value < CURRENT_CONFIG_SCHEMA_VERSION) { - m_debug.config_schema_version.value = CURRENT_CONFIG_SCHEMA_VERSION; + if (GetConfigVersion() != Common::g_scm_rev) { Save(); } - return true; - } 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, "Applying game config: {}", gamePath.string()); @@ -458,7 +352,10 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { std::vector changed; - // Apply overrides - these will set game_specific_value + // 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")) @@ -489,7 +386,6 @@ void EmulatorSettingsImpl::SetDefaultValues() { m_audio = AudioSettings{}; m_gpu = GPUSettings{}; m_vulkan = VulkanSettings{}; - m_unknown_sections.clear(); } std::vector EmulatorSettingsImpl::GetAllOverrideableKeys() const { diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index ade3974ed..00effa2bd 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -9,15 +9,12 @@ #include #include #include -#include -#include #include #include #include "common/logging/log.h" #include "common/types.h" #define EmulatorSettings (*EmulatorSettingsImpl::GetInstance()) -#define CURRENT_CONFIG_SCHEMA_VERSION 1 // Increment when adding new settings enum HideCursorState : int { Never, @@ -51,6 +48,8 @@ struct Setting { 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. @@ -67,11 +66,13 @@ struct Setting { } /// 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. + /// Discard the game-specific override; subsequent get(Default) will + /// fall back to the base value. void reset_game_specific() { game_specific_value = std::nullopt; } @@ -92,7 +93,11 @@ struct OverrideItem { 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; }; @@ -101,25 +106,40 @@ inline OverrideItem make_override(const char* key, Setting Struct::* member) return OverrideItem{ key, [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 { 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.game_specific_value = newValue; + LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key); } catch (const std::exception& e) { - LOG_ERROR(EmuSettings, "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] 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(); @@ -162,6 +182,7 @@ struct GeneralSettings { Setting show_fps_counter{false}; Setting console_language{1}; + // return a vector of override descriptors (runtime, but tiny) std::vector GetOverrideableFields() const { return std::vector{ make_override("volume_slider", &GeneralSettings::volume_slider), @@ -197,12 +218,11 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_ // Debug settings // ------------------------------- struct DebugSettings { - Setting separate_logging_enabled{false}; - Setting debug_dump{false}; - Setting shader_collect{false}; - Setting log_enabled{true}; - Setting config_version{""}; - Setting config_schema_version{CURRENT_CONFIG_SCHEMA_VERSION}; + Setting separate_logging_enabled{false}; // specific + Setting debug_dump{false}; // specific + Setting shader_collect{false}; // specific + Setting log_enabled{true}; // specific + Setting config_version{""}; // specific std::vector GetOverrideableFields() const { return std::vector{ @@ -214,22 +234,22 @@ struct DebugSettings { } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings, separate_logging_enabled, debug_dump, - shader_collect, log_enabled, config_version, - config_schema_version) + shader_collect, log_enabled, config_version) // ------------------------------- // Input settings // ------------------------------- + struct InputSettings { - Setting cursor_state{HideCursorState::Idle}; - Setting cursor_hide_timeout{5}; - Setting usb_device_backend{UsbBackendType::Real}; + Setting cursor_state{HideCursorState::Idle}; // specific + Setting cursor_hide_timeout{5}; // specific + Setting usb_device_backend{UsbBackendType::Real}; // specific Setting use_special_pad{false}; Setting special_pad_class{1}; - Setting motion_controls_enabled{true}; + Setting motion_controls_enabled{true}; // specific Setting use_unified_input_config{true}; Setting default_controller_id{""}; - Setting background_controller_input{false}; + Setting background_controller_input{false}; // specific Setting camera_id{-1}; std::vector GetOverrideableFields() const { @@ -249,7 +269,6 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings, cursor_state, cursor_hide_time usb_device_backend, use_special_pad, special_pad_class, motion_controls_enabled, use_unified_input_config, default_controller_id, background_controller_input, camera_id) - // ------------------------------- // Audio settings // ------------------------------- @@ -258,6 +277,7 @@ struct AudioSettings { Setting main_output_device{"Default Device"}; Setting padSpk_output_device{"Default Device"}; + // TODO add overrides std::vector GetOverrideableFields() const { return std::vector{ make_override("mic_device", &AudioSettings::mic_device), @@ -266,6 +286,7 @@ struct AudioSettings { &AudioSettings::padSpk_output_device)}; } }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, mic_device, main_output_device, padSpk_output_device) @@ -292,7 +313,7 @@ struct GPUSettings { Setting fsr_enabled{false}; Setting rcas_enabled{true}; Setting rcas_attenuation{250}; - + // TODO add overrides std::vector GetOverrideableFields() const { return std::vector{ make_override("null_gpu", &GPUSettings::null_gpu), @@ -323,7 +344,6 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GPUSettings, window_width, window_height, int direct_memory_access_enabled, dump_shaders, patch_shaders, vblank_frequency, full_screen, full_screen_mode, present_mode, hdr_allowed, fsr_enabled, rcas_enabled, rcas_attenuation) - // ------------------------------- // Vulkan settings // ------------------------------- @@ -339,7 +359,6 @@ struct VulkanSettings { Setting vkguest_markers{false}; Setting pipeline_cache_enabled{false}; Setting pipeline_cache_archived{false}; - std::vector GetOverrideableFields() const { return std::vector{ make_override("gpu_id", &VulkanSettings::gpu_id), @@ -384,6 +403,7 @@ public: bool Load(const std::string& serial = ""); void SetDefaultValues(); + // Config mode ConfigMode GetConfigMode() const { return m_configMode; } @@ -391,11 +411,16 @@ public: 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); std::vector GetGameInstallDirs() const; void SetAllGameInstallDirs(const std::vector& dirs); @@ -412,7 +437,47 @@ public: std::filesystem::path GetFontsDir(); void SetFontsDir(const std::filesystem::path& dir); - // Overrideable fields accessors +private: + GeneralSettings m_general{}; + DebugSettings m_debug{}; + InputSettings m_input{}; + AudioSettings m_audio{}; + GPUSettings m_gpu{}; + VulkanSettings m_vulkan{}; + ConfigMode m_configMode{ConfigMode::Default}; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; + + /// Apply overrideable fields from groupJson into group.game_specific_value. + template + void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, + std::vector& changed) { + for (auto& item : group.GetOverrideableFields()) { + if (!groupJson.contains(item.key)) + continue; + item.apply(&group, groupJson.at(item.key), changed); + } + } + + // 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: + // Add these getters to access overrideable fields std::vector GetGeneralOverrideableFields() const { return m_general.GetOverrideableFields(); } @@ -433,54 +498,6 @@ public: } std::vector GetAllOverrideableKeys() const; -private: - GeneralSettings m_general{}; - DebugSettings m_debug{}; - InputSettings m_input{}; - AudioSettings m_audio{}; - GPUSettings m_gpu{}; - VulkanSettings m_vulkan{}; - - // Store unknown top-level sections to preserve them across saves - std::unordered_map m_unknown_sections; - - ConfigMode m_configMode{ConfigMode::Default}; - - static std::shared_ptr s_instance; - static std::mutex s_mutex; - - template - void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, - std::vector& changed) { - for (auto& item : group.GetOverrideableFields()) { - if (!groupJson.contains(item.key)) - continue; - item.apply(&group, groupJson.at(item.key), changed); - } - } - - template - static void SaveGroupGameSpecific(const Group& group, nlohmann::json& out) { - for (auto& item : group.GetOverrideableFields()) - out[item.key] = item.get_for_save(&group); - } - - template - static void ClearGroupOverrides(Group& group) { - for (auto& item : group.GetOverrideableFields()) - item.reset_game_specific(&group); - } - - static void PrintChangedSummary(const std::vector& changed); - - // Set of known section names - bool IsKnownSection(const std::string& name) const { - static const std::unordered_set known_sections = { - "General", "Debug", "Input", "Audio", "GPU", "Vulkan"}; - return known_sections.find(name) != known_sections.end(); - } - -public: #define SETTING_FORWARD(group, Name, field) \ auto Get##Name() const { \ return (group).field.get(m_configMode); \ @@ -488,7 +505,6 @@ public: void Set##Name(const decltype((group).field.value)& v) { \ (group).field.value = v; \ } - #define SETTING_FORWARD_BOOL(group, Name, field) \ bool Is##Name() const { \ return (group).field.get(m_configMode); \ @@ -496,7 +512,6 @@ public: void Set##Name(bool v) { \ (group).field.value = v; \ } - #define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ bool Is##Name() const { \ return (group).field.get(m_configMode); \ @@ -532,9 +547,6 @@ public: SETTING_FORWARD_BOOL(m_debug, ShaderCollect, shader_collect) SETTING_FORWARD_BOOL(m_debug, LogEnabled, log_enabled) SETTING_FORWARD(m_debug, ConfigVersion, config_version) - int GetConfigSchemaVersion() const { - return m_debug.config_schema_version.get(m_configMode); - } // GPU Settings SETTING_FORWARD_BOOL(m_gpu, NullGPU, null_gpu) @@ -598,4 +610,4 @@ public: #undef SETTING_FORWARD #undef SETTING_FORWARD_BOOL #undef SETTING_FORWARD_BOOL_READONLY -}; \ No newline at end of file +};