diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index d0bf43f08..2faa6a0c9 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -170,7 +170,6 @@ 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) { @@ -198,34 +197,89 @@ 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"); - json j = json::object(); + // 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 = 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); @@ -241,14 +295,45 @@ 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); - json j; + m_debug.config_schema_version.value = CURRENT_CONFIG_SCHEMA_VERSION; + + // Save known sections (this will overwrite any unknown sections with the same name) 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()); @@ -273,24 +358,43 @@ 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 gj; - in >> gj; + json j; + in >> j; - auto mergeGroup = [&gj](auto& group, const char* section) { - if (!gj.contains(section)) - return; - json current = group; - current.update(gj.at(section)); - group = current.get>(); - }; + // 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(); + } - 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, "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(); + } + } LOG_DEBUG(EmuSettings, "Global config loaded successfully"); } else { @@ -314,8 +418,8 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { int result; SDL_ShowMessageBox(&msg_box, &result); if (result == 1) { - SDL_ShowSimpleMessageBox( - 0, "Error", "sike this actually isn't implemented yet lol", nullptr); + SDL_ShowSimpleMessageBox(0, "Error", "Migration not implemented yet", + nullptr); std::quick_exit(1); } } @@ -323,15 +427,17 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { SetDefaultValues(); Save(); } - if (GetConfigVersion() != Common::g_scm_rev) { + + // 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; 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()); @@ -352,10 +458,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { std::vector 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. + // Apply overrides - these will set game_specific_value if (gj.contains("General")) ApplyGroupOverrides(m_general, gj.at("General"), changed); if (gj.contains("Debug")) @@ -386,6 +489,7 @@ 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 00effa2bd..ade3974ed 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -9,12 +9,15 @@ #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, @@ -48,8 +51,6 @@ 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. @@ -66,13 +67,11 @@ 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; subsequent get(Default) will - /// fall back to the base value. + /// Discard the game-specific override. void reset_game_specific() { game_specific_value = std::nullopt; } @@ -93,11 +92,7 @@ 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; }; @@ -106,40 +101,25 @@ 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, "[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()); + LOG_ERROR(EmuSettings, "ERROR parsing {}: {}", key, e.what()); } }, - - // --- 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(); @@ -182,7 +162,6 @@ 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), @@ -218,11 +197,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_ // Debug settings // ------------------------------- struct DebugSettings { - 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 + 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}; std::vector GetOverrideableFields() const { return std::vector{ @@ -234,22 +214,22 @@ struct DebugSettings { } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings, separate_logging_enabled, debug_dump, - shader_collect, log_enabled, config_version) + shader_collect, log_enabled, config_version, + config_schema_version) // ------------------------------- // Input settings // ------------------------------- - struct InputSettings { - Setting cursor_state{HideCursorState::Idle}; // specific - Setting cursor_hide_timeout{5}; // specific - Setting usb_device_backend{UsbBackendType::Real}; // specific + Setting cursor_state{HideCursorState::Idle}; + Setting cursor_hide_timeout{5}; + Setting usb_device_backend{UsbBackendType::Real}; Setting use_special_pad{false}; Setting special_pad_class{1}; - Setting motion_controls_enabled{true}; // specific + Setting motion_controls_enabled{true}; Setting use_unified_input_config{true}; Setting default_controller_id{""}; - Setting background_controller_input{false}; // specific + Setting background_controller_input{false}; Setting camera_id{-1}; std::vector GetOverrideableFields() const { @@ -269,6 +249,7 @@ 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 // ------------------------------- @@ -277,7 +258,6 @@ 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), @@ -286,7 +266,6 @@ struct AudioSettings { &AudioSettings::padSpk_output_device)}; } }; - NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, mic_device, main_output_device, padSpk_output_device) @@ -313,7 +292,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), @@ -344,6 +323,7 @@ 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 // ------------------------------- @@ -359,6 +339,7 @@ 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), @@ -403,7 +384,6 @@ public: bool Load(const std::string& serial = ""); void SetDefaultValues(); - // Config mode ConfigMode GetConfigMode() const { return m_configMode; } @@ -411,16 +391,11 @@ 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); @@ -437,47 +412,7 @@ public: std::filesystem::path GetFontsDir(); void SetFontsDir(const std::filesystem::path& dir); -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 + // Overrideable fields accessors std::vector GetGeneralOverrideableFields() const { return m_general.GetOverrideableFields(); } @@ -498,6 +433,54 @@ 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); \ @@ -505,6 +488,7 @@ 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); \ @@ -512,6 +496,7 @@ 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); \ @@ -547,6 +532,9 @@ 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) @@ -610,4 +598,4 @@ public: #undef SETTING_FORWARD #undef SETTING_FORWARD_BOOL #undef SETTING_FORWARD_BOOL_READONLY -}; +}; \ No newline at end of file