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
#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;
}

View File

@ -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
};