diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index 1cf80430a..7137a2205 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -128,10 +128,10 @@ static auto UserPaths = [] { create_path(PathType::CustomTrophy, user_dir / CUSTOM_TROPHY); create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS); create_path(PathType::CacheDir, user_dir / CACHE_DIR); - create_path(PathType::FontDir, user_dir / FONT_DIR); + create_path(PathType::FontDir, user_dir / SYSFONTS_DIR); // subdirectory for fonts - std::filesystem::create_directory(user_dir / FONT_DIR / "font"); - std::filesystem::create_directory(user_dir / FONT_DIR / "font2"); + std::filesystem::create_directory(user_dir / SYSFONTS_DIR / "font"); + std::filesystem::create_directory(user_dir / SYSFONTS_DIR / "font2"); std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt"); if (notice_file.is_open()) { @@ -148,7 +148,7 @@ static auto UserPaths = [] { notice_file.close(); } - std::ofstream font_instructions(user_dir / FONT_DIR / "Instructions.txt"); + std::ofstream font_instructions(user_dir / SYSFONTS_DIR / "Instructions.txt"); if (font_instructions.is_open()) { font_instructions << "Place /preinst/common/font contents into font folder\n" "Place /system/common/font2 contents into font2 folder\n"; diff --git a/src/common/path_util.h b/src/common/path_util.h index f5979e3b2..00cf870e7 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -45,7 +45,7 @@ constexpr auto METADATA_DIR = "game_data"; constexpr auto CUSTOM_TROPHY = "custom_trophy"; constexpr auto CUSTOM_CONFIGS = "custom_configs"; constexpr auto CACHE_DIR = "cache"; -constexpr auto FONT_DIR = "sys_fonts"; +constexpr auto SYSFONTS_DIR = "sys_fonts"; // Filenames constexpr auto LOG_FILE = "shad_log.txt"; diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp new file mode 100644 index 000000000..49d997c49 --- /dev/null +++ b/src/core/emulator_settings.cpp @@ -0,0 +1,293 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "emulator_settings.h" + +using json = nlohmann::json; + +std::shared_ptr EmulatorSettings::s_instance = nullptr; +std::mutex EmulatorSettings::s_mutex; + +namespace nlohmann { +template <> +struct adl_serializer { + static void to_json(json& j, const std::filesystem::path& p) { + j = p.string(); + } + static void from_json(const json& j, std::filesystem::path& p) { + p = j.get(); + } +}; +} // namespace nlohmann + +// -------------------- +// Print summary +// -------------------- +void EmulatorSettings::PrintChangedSummary(const std::vector& changed) { + if (changed.empty()) { + std::cout << "[Settings] No game-specific overrides applied\n"; + return; + } + std::cout << "[Settings] Game-specific overrides applied:\n"; + for (const auto& k : changed) + std::cout << " * " << k << "\n"; +} + +// -------------------- +// ctor/dtor + singleton +// -------------------- +EmulatorSettings::EmulatorSettings() { + // Load(); +} +EmulatorSettings::~EmulatorSettings() { + Save(); +} + +std::shared_ptr EmulatorSettings::GetInstance() { + 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; +} + +// -------------------- +// General helpers +// -------------------- + +std::filesystem::path EmulatorSettings::GetSysFontsDir() { + if (m_general.sys_fonts_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::FontDir); + } + return m_general.sys_fonts_dir.value; +} + +void EmulatorSettings::SetSysFontsDir(const std::filesystem::path& dir) { + m_general.sys_fonts_dir.value = dir; +} + +// -------------------- +// Save +// -------------------- +bool EmulatorSettings::Save(const std::string& serial) const { + try { + if (!serial.empty()) { + const std::filesystem::path cfgDir = + Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs); + std::filesystem::create_directories(cfgDir); + const std::filesystem::path 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]; + } + 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]; + } + 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]; + } + 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]; + } + 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]; + } + 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]; + } + j["Vulkan"] = vulkanObj;*/ + + std::ofstream out(path); + if (!out.is_open()) { + std::cerr << "Failed to open file for writing: " << path << std::endl; + return false; + } + out << std::setw(4) << j; + out.flush(); + if (out.fail()) { + std::cerr << "Failed to write settings to: " << path << std::endl; + return false; + } + return true; + } else { + const std::filesystem::path path = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json"; + 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; + j["Users"] = m_userManager.GetUsers();*/ + + std::ofstream out(path); + if (!out.is_open()) { + std::cerr << "Failed to open file for writing: " << path << std::endl; + return false; + } + out << std::setw(4) << j; + out.flush(); + if (out.fail()) { + std::cerr << "Failed to write settings to: " << path << std::endl; + return false; + } + return true; + } + } catch (const std::exception& e) { + std::cerr << "Error saving settings: " << e.what() << std::endl; + return false; + } +} + +// -------------------- +// Load +// -------------------- +bool EmulatorSettings::Load(const std::string& serial) { + try { + const std::filesystem::path userDir = + Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const std::filesystem::path configPath = userDir / "config.json"; + + // Load global config if exists + if (std::ifstream globalIn{configPath}; globalIn.good()) { + json gj; + globalIn >> gj; + if (gj.contains("General")) { + json current = m_general; // JSON from existing struct with all defaults + current.update(gj.at("General")); // merge only fields present in file + m_general = current.get(); // convert back + } + /* if (gj.contains("Debug")) { + json current = m_debug; + current.update(gj.at("Debug")); + m_debug = current.get(); + } + if (gj.contains("Input")) { + json current = m_input; + current.update(gj.at("Input")); + m_input = current.get(); + } + if (gj.contains("Audio")) { + json current = m_audio; + current.update(gj.at("Audio")); + m_audio = current.get(); + } + if (gj.contains("GPU")) { + json current = m_gpu; + current.update(gj.at("GPU")); + m_gpu = current.get(); + } + if (gj.contains("Vulkan")) { + json current = m_vulkan; + current.update(gj.at("Vulkan")); + m_vulkan = current.get(); + } + if (gj.contains("Users")) + m_userManager.GetUsers() = gj.at("Users").get();*/ + } else { + SetDefaultValues(); + // ensure a default user exists + /* if (m_userManager.GetUsers().user.empty()) + m_userManager.GetUsers().user = m_userManager.CreateDefaultUser();*/ + Save(); + } + + // Load per-game overrides and apply + if (!serial.empty()) { + const std::filesystem::path gamePath = + Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json"); + if (!std::filesystem::exists(gamePath)) + return false; + + std::ifstream in(gamePath); + if (!in.is_open()) + return false; + + json gj; + in >> gj; + + std::vector changed; + + 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; + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Error loading settings: " << e.what() << std::endl; + return false; + } +} + +void EmulatorSettings::SetDefaultValues() { + m_general = GeneralSettings{}; + m_debug = DebugSettings{}; + m_input = InputSettings{}; + m_audio = AudioSettings{}; + m_gpu = GPUSettings{}; + m_vulkan = VulkanSettings{}; +} diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h new file mode 100644 index 000000000..73a659a32 --- /dev/null +++ b/src/core/emulator_settings.h @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "common/types.h" + +// ------------------------------- +// Generic Setting wrapper +// ------------------------------- +template +struct Setting { + T value{}; +}; + +template +void to_json(nlohmann::json& j, const Setting& s) { + j = s.value; +} + +template +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; +}; + +// Helper factory: create an OverrideItem binding a pointer-to-member +template +inline OverrideItem make_override(const char* key, Setting Struct::* member) { + return OverrideItem{key, [member, key](void* base, const nlohmann::json& entry, + std::vector& changed) { + if (!entry.is_object()) + return; + + Struct* obj = reinterpret_cast(base); + Setting& dst = obj->*member; + + Setting tmp = entry.get>(); + + if (dst.value != tmp.value) { + changed.push_back(std::string(key) + " ( " + + nlohmann::json(dst.value).dump() + " → " + + nlohmann::json(tmp.value).dump() + " )"); + } + + dst.value = tmp.value; + }}; +} + +// ------------------------------- +// General settings +// ------------------------------- +struct GeneralSettings { + Setting sys_fonts_dir; + std::vector GetOverrideableFields() const { + return std::vector{}; + } +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, sys_fonts_dir) + +// ------------------------------- +// Debug settings +// ------------------------------- +struct DebugSettings { + std::vector GetOverrideableFields() const { + return std::vector{}; + } +}; +//NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings) + +// ------------------------------- +// Input settings +// ------------------------------- + +struct InputSettings { + std::vector GetOverrideableFields() const { + return std::vector{}; + } +}; +//NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings) + +// ------------------------------- +// Audio settings +// ------------------------------- +struct AudioSettings { + std::vector GetOverrideableFields() const { + return std::vector{}; + } +}; + +//NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings) + +// ------------------------------- +// GPU settings +// ------------------------------- +struct GPUSettings { + std::vector GetOverrideableFields() const { + return std::vector{}; + } +}; +//NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GPUSettings) +// ------------------------------- +// Vulkan settings +// ------------------------------- +struct VulkanSettings { + std::vector GetOverrideableFields() const { + return std::vector{}; + } +}; +//NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(VulkanSettings) + +// ------------------------------- +// Main manager +// ------------------------------- +class EmulatorSettings { +public: + EmulatorSettings(); + ~EmulatorSettings(); + + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + + bool Save(const std::string& serial = "") const; + bool Load(const std::string& serial = ""); + void SetDefaultValues(); + + // general accessors + std::filesystem::path GetSysFontsDir(); + void SetSysFontsDir(const std::filesystem::path& dir); + +private: + GeneralSettings m_general{}; + DebugSettings m_debug{}; + InputSettings m_input{}; + AudioSettings m_audio{}; + GPUSettings m_gpu{}; + VulkanSettings m_vulkan{}; + // UserManager m_userManager; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; + + // Generic helper that applies override descriptors for a specific group + 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); + } + } + + static void PrintChangedSummary(const std::vector& changed); + +public: +#define SETTING_FORWARD(group, Name, field) \ + auto Get##Name() const { \ + return group.field.value; \ + } \ + 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; \ + } \ + void Set##Name(const decltype(group.field.value)& v) { \ + group.field.value = v; \ + } +#define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ + auto Is##Name() const { \ + return group.field.value; \ + } + +#undef SETTING_FORWARD +#undef SETTING_FORWARD_BOOL +};