mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-04-03 19:58:39 -06:00
added config mode support
This commit is contained in:
parent
a8f51584bf
commit
faf9cce67e
@ -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 <algorithm>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <common/path_util.h>
|
||||
#include "common/logging/log.h"
|
||||
#include "emulator_settings.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ── Singleton storage ─────────────────────────────────────────────────
|
||||
std::shared_ptr<EmulatorSettings> EmulatorSettings::s_instance = nullptr;
|
||||
std::mutex EmulatorSettings::s_mutex;
|
||||
|
||||
// ── nlohmann helpers for std::filesystem::path ───────────────────────
|
||||
namespace nlohmann {
|
||||
template <>
|
||||
struct adl_serializer<std::filesystem::path> {
|
||||
@ -24,40 +28,35 @@ struct adl_serializer<std::filesystem::path> {
|
||||
};
|
||||
} // namespace nlohmann
|
||||
|
||||
// --------------------
|
||||
// Print summary
|
||||
// --------------------
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
void EmulatorSettings::PrintChangedSummary(const std::vector<std::string>& 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> EmulatorSettings::GetInstance() {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
std::lock_guard lock(s_mutex);
|
||||
if (!s_instance)
|
||||
s_instance = std::make_shared<EmulatorSettings>();
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void EmulatorSettings::SetInstance(std::shared_ptr<EmulatorSettings> instance) {
|
||||
std::lock_guard<std::mutex> 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<std::remove_reference_t<decltype(group)>>();
|
||||
};
|
||||
|
||||
if (gj.contains("General")) {
|
||||
json current = m_general;
|
||||
current.update(gj.at("General"));
|
||||
m_general = current.get<GeneralSettings>();
|
||||
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Loaded General settings");
|
||||
}
|
||||
if (gj.contains("Debug")) {
|
||||
json current = m_debug;
|
||||
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")) {
|
||||
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<Users>();
|
||||
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<std::string> changed;
|
||||
|
||||
if (gj.contains("General")) {
|
||||
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying General overrides");
|
||||
ApplyGroupOverrides<GeneralSettings>(m_general, gj.at("General"), changed);
|
||||
}
|
||||
if (gj.contains("Debug")) {
|
||||
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Debug overrides");
|
||||
ApplyGroupOverrides<DebugSettings>(m_debug, gj.at("Debug"), changed);
|
||||
}
|
||||
if (gj.contains("Input")) {
|
||||
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Input overrides");
|
||||
ApplyGroupOverrides<InputSettings>(m_input, gj.at("Input"), changed);
|
||||
}
|
||||
if (gj.contains("Audio")) {
|
||||
LOG_DEBUG(EmuSettings, "[EmulatorSettings] Applying Audio overrides");
|
||||
ApplyGroupOverrides<AudioSettings>(m_audio, gj.at("Audio"), 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);
|
||||
}
|
||||
// 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<std::string> EmulatorSettings::GetAllOverrideableKeys() const {
|
||||
std::vector<std::string> keys;
|
||||
|
||||
auto addKeys = [&keys](const std::vector<OverrideItem>& 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;
|
||||
}
|
||||
@ -15,12 +15,47 @@
|
||||
#include "common/types.h"
|
||||
#include "core/user_manager.h"
|
||||
|
||||
// -------------------------------
|
||||
// Generic Setting wrapper
|
||||
// -------------------------------
|
||||
enum class ConfigMode {
|
||||
Default,
|
||||
Global,
|
||||
Clean,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Setting {
|
||||
T default_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>
|
||||
@ -33,16 +68,19 @@ void from_json(const nlohmann::json& j, Setting<T>& s) {
|
||||
s.value = j.get<T>();
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Helper to describe a per-field override action
|
||||
// -------------------------------
|
||||
struct OverrideItem {
|
||||
const char* key;
|
||||
// apply(basePtrToStruct, jsonEntry, changedFields)
|
||||
std::function<void(void*, const nlohmann::json&, std::vector<std::string>&)> apply;
|
||||
std::function<void(void* group_ptr, const nlohmann::json& entry,
|
||||
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>
|
||||
inline OverrideItem make_override(const char* key, Setting<T> Struct::* member) {
|
||||
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) {
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key);
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump());
|
||||
|
||||
Struct* obj = reinterpret_cast<Struct*>(base);
|
||||
Setting<T>& dst = obj->*member;
|
||||
|
||||
try {
|
||||
// Parse the value from JSON
|
||||
T newValue = entry.get<T>();
|
||||
|
||||
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<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 = "");
|
||||
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<std::filesystem::path> 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<EmulatorSettings> 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 <typename Group>
|
||||
void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson,
|
||||
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);
|
||||
|
||||
public:
|
||||
@ -407,23 +487,24 @@ public:
|
||||
return m_vulkan.GetOverrideableFields();
|
||||
}
|
||||
std::vector<std::string> 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
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user