diff --git a/CMakeLists.txt b/CMakeLists.txt index 38341e81e..943609923 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +# SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later # Version 3.24 needed for FetchContent OVERRIDE_FIND_PACKAGE @@ -848,6 +848,8 @@ set(CORE src/core/aerolib/stubs.cpp src/core/tls.h src/core/emulator_state.cpp src/core/emulator_state.h + src/core/emulator_settings.cpp + src/core/emulator_settings.h ) if (ARCHITECTURE STREQUAL "x86_64") diff --git a/src/common/config.cpp b/src/common/config.cpp index 34e5524cf..5159c9cf3 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -239,6 +239,9 @@ void setSysModulesPath(const std::filesystem::path& path) { } std::filesystem::path getSysFontPath() { + if (sys_font_path.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::FontDir); + } return sys_font_path; } @@ -502,7 +505,7 @@ void setShowFpsCounter(bool enable, bool is_game_specific) { showFpsCounter.set(enable, is_game_specific); } -bool isLoggingEnabled() { +static bool isLoggingEnabled() { return logEnabled.get(); } @@ -1088,7 +1091,7 @@ void load(const std::filesystem::path& path, bool is_game_specific) { } } -void sortTomlSections(toml::ordered_value& data) { +static void sortTomlSections(toml::ordered_value& data) { toml::ordered_value ordered_data; std::vector section_order = {"General", "Input", "Audio", "GPU", "Vulkan", "Debug", "Keys", "GUI", "Settings"}; @@ -1401,7 +1404,7 @@ hotkey_quit = lctrl, lshift, end )"; } -constexpr std::string_view GetDefaultInputConfig() { +static constexpr std::string_view GetDefaultInputConfig() { return R"(#Feeling lost? Check out the Help section! # Keyboard bindings diff --git a/src/common/config.h b/src/common/config.h index 9903e6e35..2a95e6cf0 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -4,8 +4,6 @@ #pragma once #include -#include -#include #include #include "types.h" @@ -155,15 +153,6 @@ void setConnectedToNetwork(bool enable, bool is_game_specific = false); void setUserName(const std::string& name, bool is_game_specific = false); std::filesystem::path getSysModulesPath(); void setSysModulesPath(const std::filesystem::path& path); -std::filesystem::path getSysFontPath(); -void setSysFontPath(const std::filesystem::path& path); -std::optional getSystemFontOverride(std::string_view key); -std::string getSystemFontFallbackName(); -void setSystemFontFallbackName(const std::string& name); -void setSystemFontOverride(std::string_view key, const std::filesystem::path& path); -void clearSystemFontOverrides(); -bool getLoadAutoPatches(); -void setLoadAutoPatches(bool enable); enum UsbBackendType : int { Real, SkylandersPortal, InfinityBase, DimensionsToypad }; int getUsbDeviceBackend(); diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index b0cbb10cf..0415c1a26 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -128,6 +128,9 @@ 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 / SYSFONTS_DIR); + 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()) { @@ -144,6 +147,17 @@ static auto UserPaths = [] { notice_file.close(); } + const auto instructions_path = user_dir / SYSFONTS_DIR / "Instructions.txt"; + std::error_code ec; + if (!std::filesystem::exists(instructions_path, ec)) { + std::ofstream font_instructions(instructions_path); + if (font_instructions.is_open()) { + font_instructions << "Place system font files (.otf/.ttf) into the 'font' and 'font2' " + "folders.\n"; + font_instructions.close(); + } + } + return paths; }(); diff --git a/src/common/path_util.h b/src/common/path_util.h index fd2c18baa..cf4611903 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -25,6 +25,7 @@ enum class PathType { CustomTrophy, // Where custom files for trophies are stored. CustomConfigs, // Where custom files for different games are stored. CacheDir, // Where pipeline and shader cache is stored. + FontDir, // Where system font files are stored. }; constexpr auto PORTABLE_DIR = "user"; @@ -44,6 +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 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 +}; diff --git a/src/core/libraries/font/font_internal.cpp b/src/core/libraries/font/font_internal.cpp index a6684d2be..11a3c85fc 100644 --- a/src/core/libraries/font/font_internal.cpp +++ b/src/core/libraries/font/font_internal.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "font_internal.h" @@ -10,6 +10,7 @@ #include FT_OUTLINE_H #include FT_TRUETYPE_TABLES_H +#include "core/emulator_settings.h" #include "core/libraries/font/fontft_internal.h" namespace Libraries::Font::Internal { @@ -1559,7 +1560,7 @@ static std::optional FindChildDirContainingFile( } std::filesystem::path GetSysFontBaseDir() { - std::filesystem::path base = Config::getSysFontPath(); + std::filesystem::path base = EmulatorSettings::GetInstance()->GetSysFontsDir(); std::error_code ec; if (base.empty()) { LOG_ERROR(Lib_Font, "SystemFonts: SysFontPath not set"); @@ -1992,7 +1993,7 @@ std::string ReportSystemFaceRequest(FontState& st, Libraries::Font::OrbisFontHan } if (!st.system_requested) { st.system_requested = true; - const auto configured = Config::getSysFontPath(); + const auto configured = EmulatorSettings::GetInstance()->GetSysFontsDir(); return fmt::format("SystemFace: handle={} requested internal font but sysFontPath ('{}') " "could not be loaded", static_cast(handle), configured.string()); diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 7f679e7c2..63e8d7467 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -12,6 +12,8 @@ #include "core/libraries/companion/companion_httpd.h" #include "core/libraries/companion/companion_util.h" #include "core/libraries/disc_map/disc_map.h" +#include "core/libraries/font/font.h" +#include "core/libraries/font/fontft.h" #include "core/libraries/game_live_streaming/gamelivestreaming.h" #include "core/libraries/gnmdriver/gnmdriver.h" #include "core/libraries/hmd/hmd.h" @@ -142,6 +144,8 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Voice::RegisterLib(sym); Libraries::Rudp::RegisterLib(sym); Libraries::VrTracker::RegisterLib(sym); + Libraries::Font::RegisterlibSceFont(sym); + Libraries::FontFt::RegisterlibSceFontFt(sym); // Loading libSceSsl is locked behind a title workaround that currently applies to nothing. // Libraries::Ssl::RegisterLib(sym); diff --git a/src/emulator.cpp b/src/emulator.cpp index 263bd9c2b..1403b1254 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -32,8 +32,6 @@ #include "core/file_format/trp.h" #include "core/file_sys/fs.h" #include "core/libraries/disc_map/disc_map.h" -#include "core/libraries/font/font.h" -#include "core/libraries/font/fontft.h" #include "core/libraries/jpeg/jpegenc.h" #include "core/libraries/libc_internal/libc_internal.h" #include "core/libraries/libs.h" @@ -531,11 +529,7 @@ void Emulator::LoadSystemModules(const std::string& game_serial) { {"libSceJson.sprx", nullptr}, {"libSceJson2.sprx", nullptr}, {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, - {"libSceCesCs.sprx", nullptr}, - {"libSceAudiodec.sprx", nullptr}, - {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, - {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, - {"libSceFreeTypeOt.sprx", nullptr}}); + {"libSceCesCs.sprx", nullptr}}); std::vector found_modules; const auto& sys_module_path = Config::getSysModulesPath(); diff --git a/src/main.cpp b/src/main.cpp index aa3f4de45..080fc94a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,7 @@ #ifdef _WIN32 #include #endif +#include int main(int argc, char* argv[]) { #ifdef _WIN32 @@ -32,6 +33,10 @@ int main(int argc, char* argv[]) { std::shared_ptr m_emu_state = std::make_shared(); EmulatorState::SetInstance(m_emu_state); // Load configurations + std::shared_ptr emu_settings = std::make_shared(); + EmulatorSettings::SetInstance(emu_settings); + emu_settings->Load(); + const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml");