mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2025-12-09 13:19:00 +00:00
initial
This commit is contained in:
@@ -833,6 +833,10 @@ set(CORE src/core/aerolib/stubs.cpp
|
||||
src/core/thread.h
|
||||
src/core/tls.cpp
|
||||
src/core/tls.h
|
||||
src/core/emulator_settings.cpp
|
||||
src/core/emulator_settings.h
|
||||
src/core/user_manager.cpp
|
||||
src/core/user_manager.h
|
||||
)
|
||||
|
||||
if (ARCHITECTURE STREQUAL "x86_64")
|
||||
|
||||
@@ -130,7 +130,6 @@ public:
|
||||
|
||||
// General
|
||||
static ConfigEntry<int> volumeSlider(100);
|
||||
static ConfigEntry<bool> isNeo(false);
|
||||
static ConfigEntry<bool> isDevKit(false);
|
||||
static ConfigEntry<int> extraDmemInMbytes(0);
|
||||
static ConfigEntry<bool> isPSNSignedIn(false);
|
||||
@@ -195,7 +194,6 @@ static ConfigEntry<bool> rdocEnable(false);
|
||||
// Debug
|
||||
static ConfigEntry<bool> isDebugDump(false);
|
||||
static ConfigEntry<bool> isShaderDebug(false);
|
||||
static ConfigEntry<bool> isSeparateLogFilesEnabled(false);
|
||||
static ConfigEntry<bool> isFpsColor(true);
|
||||
static ConfigEntry<bool> logEnabled(true);
|
||||
|
||||
@@ -298,10 +296,6 @@ void setVolumeSlider(int volumeValue, bool is_game_specific) {
|
||||
volumeSlider.set(volumeValue, is_game_specific);
|
||||
}
|
||||
|
||||
bool isNeoModeConsole() {
|
||||
return isNeo.get();
|
||||
}
|
||||
|
||||
bool isDevKitConsole() {
|
||||
return isDevKit.get();
|
||||
}
|
||||
@@ -655,10 +649,6 @@ void setLanguage(u32 language, bool is_game_specific) {
|
||||
m_language.set(language, is_game_specific);
|
||||
}
|
||||
|
||||
void setNeoMode(bool enable, bool is_game_specific) {
|
||||
isNeo.set(enable, is_game_specific);
|
||||
}
|
||||
|
||||
void setDevKitConsole(bool enable, bool is_game_specific) {
|
||||
isDevKit.set(enable, is_game_specific);
|
||||
}
|
||||
@@ -671,10 +661,6 @@ void setLogFilter(const string& type, bool is_game_specific) {
|
||||
logFilter.set(type, is_game_specific);
|
||||
}
|
||||
|
||||
void setSeparateLogFilesEnabled(bool enabled, bool is_game_specific) {
|
||||
isSeparateLogFilesEnabled.set(enabled, is_game_specific);
|
||||
}
|
||||
|
||||
void setUserName(const string& name, bool is_game_specific) {
|
||||
userName.set(name, is_game_specific);
|
||||
}
|
||||
@@ -768,10 +754,6 @@ u32 GetLanguage() {
|
||||
return m_language.get();
|
||||
}
|
||||
|
||||
bool getSeparateLogFilesEnabled() {
|
||||
return isSeparateLogFilesEnabled.get();
|
||||
}
|
||||
|
||||
bool getPSNSignedIn() {
|
||||
return isPSNSignedIn.get();
|
||||
}
|
||||
@@ -861,7 +843,6 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
|
||||
const toml::value& general = data.at("General");
|
||||
|
||||
volumeSlider.setFromToml(general, "volumeSlider", is_game_specific);
|
||||
isNeo.setFromToml(general, "isPS4Pro", is_game_specific);
|
||||
isDevKit.setFromToml(general, "isDevKit", is_game_specific);
|
||||
if (is_game_specific) { // do not get this value from the base config
|
||||
extraDmemInMbytes.setFromToml(general, "extraDmemInMbytes", is_game_specific);
|
||||
@@ -946,7 +927,6 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
|
||||
const toml::value& debug = data.at("Debug");
|
||||
|
||||
isDebugDump.setFromToml(debug, "DebugDump", is_game_specific);
|
||||
isSeparateLogFilesEnabled.setFromToml(debug, "isSeparateLogFilesEnabled", is_game_specific);
|
||||
isShaderDebug.setFromToml(debug, "CollectShader", is_game_specific);
|
||||
isFpsColor.setFromToml(debug, "FPSColor", is_game_specific);
|
||||
logEnabled.setFromToml(debug, "logEnabled", is_game_specific);
|
||||
@@ -1061,7 +1041,6 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
|
||||
userName.setTomlValue(data, "General", "userName", is_game_specific);
|
||||
isShowSplash.setTomlValue(data, "General", "showSplash", is_game_specific);
|
||||
isSideTrophy.setTomlValue(data, "General", "sideTrophy", is_game_specific);
|
||||
isNeo.setTomlValue(data, "General", "isPS4Pro", is_game_specific);
|
||||
isDevKit.setTomlValue(data, "General", "isDevKit", is_game_specific);
|
||||
if (is_game_specific) {
|
||||
extraDmemInMbytes.setTomlValue(data, "General", "extraDmemInMbytes", is_game_specific);
|
||||
@@ -1110,8 +1089,6 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
|
||||
|
||||
isDebugDump.setTomlValue(data, "Debug", "DebugDump", is_game_specific);
|
||||
isShaderDebug.setTomlValue(data, "Debug", "CollectShader", is_game_specific);
|
||||
isSeparateLogFilesEnabled.setTomlValue(data, "Debug", "isSeparateLogFilesEnabled",
|
||||
is_game_specific);
|
||||
logEnabled.setTomlValue(data, "Debug", "logEnabled", is_game_specific);
|
||||
|
||||
m_language.setTomlValue(data, "Settings", "consoleLanguage", is_game_specific);
|
||||
@@ -1183,7 +1160,6 @@ void setDefaultValues(bool is_game_specific) {
|
||||
if (is_game_specific) {
|
||||
readbacksEnabled.set(false, is_game_specific);
|
||||
readbackLinearImagesEnabled.set(false, is_game_specific);
|
||||
isNeo.set(false, is_game_specific);
|
||||
isDevKit.set(false, is_game_specific);
|
||||
isPSNSignedIn.set(false, is_game_specific);
|
||||
isConnectedToNetwork.set(false, is_game_specific);
|
||||
@@ -1241,7 +1217,6 @@ void setDefaultValues(bool is_game_specific) {
|
||||
// GS - Debug
|
||||
isDebugDump.set(false, is_game_specific);
|
||||
isShaderDebug.set(false, is_game_specific);
|
||||
isSeparateLogFilesEnabled.set(false, is_game_specific);
|
||||
logEnabled.set(true, is_game_specific);
|
||||
|
||||
// GS - Settings
|
||||
|
||||
@@ -110,8 +110,6 @@ void setPadSpkOutputDevice(std::string device, bool is_game_specific = false);
|
||||
std::string getMicDevice();
|
||||
void setCursorHideTimeout(int newcursorHideTimeout, bool is_game_specific = false);
|
||||
void setMicDevice(std::string device, bool is_game_specific = false);
|
||||
void setSeparateLogFilesEnabled(bool enabled, bool is_game_specific = false);
|
||||
bool getSeparateLogFilesEnabled();
|
||||
u32 GetLanguage();
|
||||
void setLanguage(u32 language, bool is_game_specific = false);
|
||||
void setUseSpecialPad(bool use);
|
||||
@@ -122,8 +120,6 @@ bool getPSNSignedIn();
|
||||
void setPSNSignedIn(bool sign, bool is_game_specific = false);
|
||||
bool patchShaders(); // no set
|
||||
bool fpsColor(); // no set
|
||||
bool isNeoModeConsole();
|
||||
void setNeoMode(bool enable, bool is_game_specific = false);
|
||||
bool isDevKitConsole();
|
||||
void setDevKitConsole(bool enable, bool is_game_specific = false);
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ static auto UserPaths = [] {
|
||||
create_path(PathType::MetaDataDir, user_dir / METADATA_DIR);
|
||||
create_path(PathType::CustomTrophy, user_dir / CUSTOM_TROPHY);
|
||||
create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS);
|
||||
create_path(PathType::HomeDir, user_dir / HOME_DIR);
|
||||
|
||||
std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt");
|
||||
if (notice_file.is_open()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ enum class PathType {
|
||||
MetaDataDir, // Where game metadata (e.g. trophies and menu backgrounds) is stored.
|
||||
CustomTrophy, // Where custom files for trophies are stored.
|
||||
CustomConfigs, // Where custom files for different games are stored.
|
||||
HomeDir, // PS4 home directory
|
||||
};
|
||||
|
||||
constexpr auto PORTABLE_DIR = "user";
|
||||
@@ -42,6 +43,7 @@ constexpr auto PATCHES_DIR = "patches";
|
||||
constexpr auto METADATA_DIR = "game_data";
|
||||
constexpr auto CUSTOM_TROPHY = "custom_trophy";
|
||||
constexpr auto CUSTOM_CONFIGS = "custom_configs";
|
||||
constexpr auto HOME_DIR = "home";
|
||||
|
||||
// Filenames
|
||||
constexpr auto LOG_FILE = "shad_log.txt";
|
||||
|
||||
321
src/core/emulator_settings.cpp
Normal file
321
src/core/emulator_settings.cpp
Normal file
@@ -0,0 +1,321 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <common/path_util.h>
|
||||
#include "emulator_settings.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
std::shared_ptr<EmulatorSettings> EmulatorSettings::s_instance = nullptr;
|
||||
std::mutex EmulatorSettings::s_mutex;
|
||||
|
||||
namespace nlohmann {
|
||||
template <>
|
||||
struct adl_serializer<std::filesystem::path> {
|
||||
static void to_json(json& j, const std::filesystem::path& p) {
|
||||
j = p.u8string();
|
||||
}
|
||||
static void from_json(const json& j, std::filesystem::path& p) {
|
||||
p = j.get<std::string>();
|
||||
}
|
||||
};
|
||||
} // namespace nlohmann
|
||||
|
||||
// --------------------
|
||||
// Print summary
|
||||
// --------------------
|
||||
void EmulatorSettings::PrintChangedSummary(const std::vector<std::string>& 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> EmulatorSettings::GetInstance() {
|
||||
std::lock_guard<std::mutex> 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;
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// General helpers
|
||||
// --------------------
|
||||
bool EmulatorSettings::AddGameInstallDir(const std::filesystem::path& dir, bool enabled) {
|
||||
for (const auto& d : m_general.install_dirs.value)
|
||||
if (d.path == dir)
|
||||
return false;
|
||||
m_general.install_dirs.value.push_back({dir, enabled});
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> EmulatorSettings::GetGameInstallDirs() const {
|
||||
std::vector<std::filesystem::path> out;
|
||||
for (const auto& d : m_general.install_dirs.value)
|
||||
if (d.enabled)
|
||||
out.push_back(d.path);
|
||||
return out;
|
||||
}
|
||||
|
||||
void EmulatorSettings::SetAllGameInstallDirs(const std::vector<GameInstallDir>& dirs) {
|
||||
m_general.install_dirs.value = dirs;
|
||||
}
|
||||
|
||||
std::filesystem::path EmulatorSettings::GetHomeDir() {
|
||||
if (m_general.home_dir.value.empty()) {
|
||||
return Common::FS::GetUserPath(Common::FS::PathType::HomeDir);
|
||||
}
|
||||
return m_general.home_dir.value;
|
||||
}
|
||||
|
||||
void EmulatorSettings::SetHomeDir(const std::filesystem::path& dir) {
|
||||
m_general.home_dir.value = dir;
|
||||
}
|
||||
|
||||
std::filesystem::path EmulatorSettings::GetSysModulesDir() {
|
||||
if (m_general.sys_modules_dir.value.empty()) {
|
||||
return Common::FS::GetUserPath(Common::FS::PathType::SysModuleDir);
|
||||
}
|
||||
return m_general.sys_modules_dir.value;
|
||||
}
|
||||
|
||||
void EmulatorSettings::SetSysModulesDir(const std::filesystem::path& dir) {
|
||||
m_general.sys_modules_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<GeneralSettings>(); // convert back
|
||||
}
|
||||
if (gj.contains("Debug")) {
|
||||
json current = m_debug;
|
||||
current.update(gj.at("Debug"));
|
||||
m_debug = current.get<DebugSettings>();
|
||||
}
|
||||
if (gj.contains("Input")) {
|
||||
json current = m_input;
|
||||
current.update(gj.at("Input"));
|
||||
m_input = current.get<InputSettings>();
|
||||
}
|
||||
if (gj.contains("Audio")) {
|
||||
json current = m_audio;
|
||||
current.update(gj.at("Audio"));
|
||||
m_audio = current.get<AudioSettings>();
|
||||
}
|
||||
if (gj.contains("GPU")) {
|
||||
json current = m_gpu;
|
||||
current.update(gj.at("GPU"));
|
||||
m_gpu = current.get<GPUSettings>();
|
||||
}
|
||||
if (gj.contains("Vulkan")) {
|
||||
json current = m_vulkan;
|
||||
current.update(gj.at("Vulkan"));
|
||||
m_vulkan = current.get<VulkanSettings>();
|
||||
}
|
||||
if (gj.contains("Users"))
|
||||
m_userManager.GetUsers() = gj.at("Users").get<Users>();
|
||||
} else {
|
||||
// ensure a default user exists
|
||||
if (m_userManager.GetUsers().user.empty())
|
||||
m_userManager.GetUsers().user = m_userManager.CreateDefaultUser();
|
||||
}
|
||||
|
||||
// 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<std::string> changed;
|
||||
|
||||
if (gj.contains("General")) {
|
||||
ApplyGroupOverrides<GeneralSettings>(m_general, gj.at("General"), changed);
|
||||
}
|
||||
if (gj.contains("Debug")) {
|
||||
ApplyGroupOverrides<DebugSettings>(m_debug, gj.at("Debug"), changed);
|
||||
}
|
||||
if (gj.contains("Input")) {
|
||||
ApplyGroupOverrides<InputSettings>(m_input, gj.at("Input"), changed);
|
||||
}
|
||||
if (gj.contains("Audio")) {
|
||||
ApplyGroupOverrides<AudioSettings>(m_audio, gj.at("Audio"), changed);
|
||||
}
|
||||
if (gj.contains("GPU")) {
|
||||
ApplyGroupOverrides<GPUSettings>(m_gpu, gj.at("GPU"), changed);
|
||||
}
|
||||
if (gj.contains("Vulkan")) {
|
||||
ApplyGroupOverrides<VulkanSettings>(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{};
|
||||
}
|
||||
328
src/core/emulator_settings.h
Normal file
328
src/core/emulator_settings.h
Normal file
@@ -0,0 +1,328 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "common/types.h"
|
||||
#include "core/user_manager.h"
|
||||
|
||||
// -------------------------------
|
||||
// Generic Setting wrapper
|
||||
// -------------------------------
|
||||
template <typename T>
|
||||
struct Setting {
|
||||
T value{};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void to_json(nlohmann::json& j, const Setting<T>& s) {
|
||||
j = s.value;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
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;
|
||||
};
|
||||
|
||||
// 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{key, [member, key](void* base, const nlohmann::json& entry,
|
||||
std::vector<std::string>& changed) {
|
||||
if (!entry.is_object())
|
||||
return;
|
||||
|
||||
Struct* obj = reinterpret_cast<Struct*>(base);
|
||||
Setting<T>& dst = obj->*member;
|
||||
|
||||
Setting<T> tmp = entry.get<Setting<T>>();
|
||||
|
||||
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;
|
||||
}};
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Support types
|
||||
// -------------------------------
|
||||
struct GameInstallDir {
|
||||
std::filesystem::path path;
|
||||
bool enabled;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GameInstallDir, path, enabled)
|
||||
|
||||
// -------------------------------
|
||||
// General settings
|
||||
// -------------------------------
|
||||
struct GeneralSettings {
|
||||
Setting<std::vector<GameInstallDir>> install_dirs;
|
||||
Setting<std::filesystem::path> addon_install_dir;
|
||||
Setting<std::filesystem::path> home_dir;
|
||||
Setting<std::filesystem::path> sys_modules_dir;
|
||||
|
||||
Setting<int> volume_slider{100};
|
||||
Setting<bool> neo_mode{false};
|
||||
Setting<bool> dev_kit_mode{false};
|
||||
Setting<int> extra_dmem_in_mbytes{0};
|
||||
Setting<bool> psn_signed_in{false};
|
||||
Setting<bool> trophy_popup_disabled{false};
|
||||
Setting<double> trophy_notification_duration{6.0};
|
||||
Setting<std::string> log_filter{""};
|
||||
Setting<std::string> log_type{"sync"};
|
||||
Setting<bool> show_splash{false};
|
||||
Setting<std::string> side_trophy{"right"};
|
||||
Setting<bool> connected_to_network{false};
|
||||
Setting<bool> discord_rpc_enabled{false};
|
||||
|
||||
// return a vector of override descriptors (runtime, but tiny)
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{
|
||||
make_override<GeneralSettings>("volume_slider", &GeneralSettings::volume_slider),
|
||||
make_override<GeneralSettings>("neo_mode", &GeneralSettings::neo_mode),
|
||||
make_override<GeneralSettings>("dev_kit_mode", &GeneralSettings::dev_kit_mode),
|
||||
make_override<GeneralSettings>("extra_dmem_in_mbytes",
|
||||
&GeneralSettings::extra_dmem_in_mbytes),
|
||||
make_override<GeneralSettings>("psn_signed_in", &GeneralSettings::psn_signed_in),
|
||||
make_override<GeneralSettings>("trophy_popup_disabled",
|
||||
&GeneralSettings::trophy_popup_disabled),
|
||||
make_override<GeneralSettings>("trophy_notification_duration",
|
||||
&GeneralSettings::trophy_notification_duration),
|
||||
make_override<GeneralSettings>("log_filter", &GeneralSettings::log_filter),
|
||||
make_override<GeneralSettings>("log_type", &GeneralSettings::log_type),
|
||||
make_override<GeneralSettings>("show_splash", &GeneralSettings::show_splash),
|
||||
make_override<GeneralSettings>("side_trophy", &GeneralSettings::side_trophy),
|
||||
make_override<GeneralSettings>("connected_to_network",
|
||||
&GeneralSettings::connected_to_network)};
|
||||
}
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_dir, home_dir,
|
||||
sys_modules_dir, volume_slider, neo_mode, dev_kit_mode,
|
||||
extra_dmem_in_mbytes, psn_signed_in, trophy_popup_disabled,
|
||||
trophy_notification_duration, log_filter, log_type, show_splash,
|
||||
side_trophy, connected_to_network, discord_rpc_enabled)
|
||||
|
||||
// -------------------------------
|
||||
// Debug settings
|
||||
// -------------------------------
|
||||
struct DebugSettings {
|
||||
Setting<bool> separate_logging_enabled{false}; // specific
|
||||
Setting<bool> debug_dump{false}; // specific
|
||||
Setting<bool> shader_debug{false}; // specific
|
||||
Setting<bool> fps_color{true};
|
||||
Setting<bool> log_enabled{true}; // specific
|
||||
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{
|
||||
make_override<DebugSettings>("debug_dump", &DebugSettings::debug_dump),
|
||||
make_override<DebugSettings>("shader_debug", &DebugSettings::shader_debug),
|
||||
make_override<DebugSettings>("separate_logging_enabled",
|
||||
&DebugSettings::separate_logging_enabled),
|
||||
make_override<DebugSettings>("log_enabled", &DebugSettings::log_enabled)};
|
||||
}
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings, separate_logging_enabled, debug_dump,
|
||||
shader_debug, fps_color, log_enabled)
|
||||
|
||||
// -------------------------------
|
||||
// Input settings
|
||||
// -------------------------------
|
||||
enum HideCursorState : int { Never, Idle, Always };
|
||||
|
||||
struct InputSettings {
|
||||
Setting<int> cursor_state{HideCursorState::Idle}; // specific
|
||||
Setting<int> cursor_hide_timeout{5}; // specific
|
||||
Setting<bool> use_special_pad{false};
|
||||
Setting<int> special_pad_class{1};
|
||||
Setting<bool> motion_controls_enabled{true}; // specific
|
||||
Setting<bool> use_unified_Input_Config{true};
|
||||
Setting<std::string> default_controller_id{""};
|
||||
Setting<bool> background_controller_input{false}; // specific
|
||||
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{
|
||||
make_override<InputSettings>("cursor_state", &InputSettings::cursor_state),
|
||||
make_override<InputSettings>("cursor_hide_timeout",
|
||||
&InputSettings::cursor_hide_timeout),
|
||||
make_override<InputSettings>("motion_controls_enabled",
|
||||
&InputSettings::motion_controls_enabled),
|
||||
make_override<InputSettings>("background_controller_input",
|
||||
&InputSettings::background_controller_input)};
|
||||
}
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings, cursor_state, cursor_hide_timeout,
|
||||
use_special_pad, special_pad_class, motion_controls_enabled,
|
||||
use_unified_Input_Config, default_controller_id,
|
||||
background_controller_input)
|
||||
// -------------------------------
|
||||
// Audio settings
|
||||
// -------------------------------
|
||||
struct AudioSettings {
|
||||
Setting<std::string> mic_device{"Default Device"};
|
||||
Setting<std::string> main_output_device{"Default Device"};
|
||||
Setting<std::string> padSpk_output_device{"Default Device"};
|
||||
|
||||
// TODO add overrides
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{};
|
||||
}
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, mic_device, main_output_device,
|
||||
padSpk_output_device)
|
||||
|
||||
// -------------------------------
|
||||
// GPU settings
|
||||
// -------------------------------
|
||||
struct GPUSettings {
|
||||
Setting<u32> window_width{1280};
|
||||
Setting<u32> window_height{720};
|
||||
Setting<u32> internal_screen_width{1280};
|
||||
Setting<u32> internal_screen_height{720};
|
||||
Setting<bool> null_gpu{false};
|
||||
Setting<bool> should_copy_gpu_buffers{false};
|
||||
Setting<bool> readbacks_enabled{false};
|
||||
Setting<bool> readback_linear_images_enabled{false};
|
||||
Setting<bool> direct_memory_access_enabled{false};
|
||||
Setting<bool> should_dump_shaders{false};
|
||||
Setting<bool> should_patch_shaders{false};
|
||||
Setting<u32> vblank_frequency{60};
|
||||
Setting<bool> full_screen{false};
|
||||
Setting<std::string> full_screen_mode{"Windowed"};
|
||||
Setting<std::string> present_mode{"Mailbox"};
|
||||
Setting<bool> hdr_allowed{false};
|
||||
Setting<bool> fsr_enabled{false};
|
||||
Setting<bool> rcas_enabled{true};
|
||||
Setting<int> rcas_attenuation{250};
|
||||
// TODO add overrides
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{};
|
||||
}
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GPUSettings, window_width, window_height, internal_screen_width,
|
||||
internal_screen_height, null_gpu, should_copy_gpu_buffers,
|
||||
readbacks_enabled, readback_linear_images_enabled,
|
||||
direct_memory_access_enabled, should_dump_shaders,
|
||||
should_patch_shaders, vblank_frequency, full_screen,
|
||||
full_screen_mode, present_mode, hdr_allowed, fsr_enabled,
|
||||
rcas_enabled, rcas_attenuation)
|
||||
// -------------------------------
|
||||
// Vulkan settings
|
||||
// -------------------------------
|
||||
struct VulkanSettings {
|
||||
Setting<s32> gpu_id{-1};
|
||||
// TODO
|
||||
std::vector<OverrideItem> GetOverrideableFields() const {
|
||||
return std::vector<OverrideItem>{};
|
||||
}
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(VulkanSettings, gpu_id)
|
||||
// -------------------------------
|
||||
// User settings
|
||||
// -------------------------------
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, user_id, user_color, user_name, controller_port)
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Users, default_user_id, user)
|
||||
|
||||
// -------------------------------
|
||||
// Main manager
|
||||
// -------------------------------
|
||||
class EmulatorSettings {
|
||||
public:
|
||||
EmulatorSettings();
|
||||
~EmulatorSettings();
|
||||
|
||||
static std::shared_ptr<EmulatorSettings> GetInstance();
|
||||
static void SetInstance(std::shared_ptr<EmulatorSettings> instance);
|
||||
|
||||
bool Save(const std::string& serial = "") const;
|
||||
bool Load(const std::string& serial = "");
|
||||
void setDefaultValues();
|
||||
|
||||
// general accessors
|
||||
bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true);
|
||||
std::vector<std::filesystem::path> GetGameInstallDirs() const;
|
||||
void SetAllGameInstallDirs(const std::vector<GameInstallDir>& dirs);
|
||||
std::filesystem::path GetHomeDir();
|
||||
void SetHomeDir(const std::filesystem::path& dir);
|
||||
std::filesystem::path GetSysModulesDir();
|
||||
void SetSysModulesDir(const std::filesystem::path& dir);
|
||||
|
||||
// user helpers
|
||||
UserManager& GetUserManager() {
|
||||
return m_userManager;
|
||||
}
|
||||
const UserManager& GetUserManager() const {
|
||||
return m_userManager;
|
||||
}
|
||||
|
||||
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<EmulatorSettings> s_instance;
|
||||
static std::mutex s_mutex;
|
||||
|
||||
// Generic helper that applies override descriptors for a specific group
|
||||
template <typename Group>
|
||||
void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson,
|
||||
std::vector<std::string>& 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<std::string>& 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; \
|
||||
}
|
||||
// General settings
|
||||
SETTING_FORWARD(m_general, VolumeSlider, volume_slider)
|
||||
SETTING_FORWARD_BOOL(m_general, Neo, neo_mode)
|
||||
SETTING_FORWARD(m_general, AddonInstallDir, addon_install_dir)
|
||||
|
||||
// Debug settings
|
||||
SETTING_FORWARD_BOOL(m_debug, SeparateLoggingEnabled, separate_logging_enabled)
|
||||
|
||||
#undef SETTING_FORWARD
|
||||
#undef SETTING_FORWARD_BOOL
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "common/config.h"
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/file_sys/fs.h"
|
||||
#include "core/libraries/kernel/orbis_error.h"
|
||||
#include "core/libraries/kernel/process.h"
|
||||
@@ -17,19 +18,19 @@ s32 PS4_SYSV_ABI sceKernelIsInSandbox() {
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceKernelIsNeoMode() {
|
||||
return Config::isNeoModeConsole() &&
|
||||
return EmulatorSettings::GetInstance()->IsNeo() &&
|
||||
Common::ElfInfo::Instance().GetPSFAttributes().support_neo_mode;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceKernelHasNeoMode() {
|
||||
return Config::isNeoModeConsole();
|
||||
return EmulatorSettings::GetInstance()->IsNeo();
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceKernelGetMainSocId() {
|
||||
// These hardcoded values are based on hardware observations.
|
||||
// Different models of PS4/PS4 Pro likely return slightly different values.
|
||||
LOG_DEBUG(Lib_Kernel, "called");
|
||||
if (Config::isNeoModeConsole()) {
|
||||
if (EmulatorSettings::GetInstance()->IsNeo()) {
|
||||
return 0x740f30;
|
||||
}
|
||||
return 0x710f10;
|
||||
|
||||
112
src/core/user_manager.cpp
Normal file
112
src/core/user_manager.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <common/path_util.h>
|
||||
#include "emulator_settings.h"
|
||||
#include "user_manager.h"
|
||||
|
||||
bool UserManager::AddUser(const User& user) {
|
||||
for (const auto& u : m_users.user) {
|
||||
if (u.user_id == user.user_id)
|
||||
return false; // already exists
|
||||
}
|
||||
|
||||
m_users.user.push_back(user);
|
||||
|
||||
// Create user home directory and subfolders
|
||||
const auto user_dir =
|
||||
EmulatorSettings::GetInstance()->GetHomeDir() / std::to_string(user.user_id);
|
||||
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(user_dir)) {
|
||||
std::filesystem::create_directory(user_dir, ec);
|
||||
std::filesystem::create_directory(user_dir / "savedata", ec);
|
||||
std::filesystem::create_directory(user_dir / "trophy", ec);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UserManager::RemoveUser(s32 user_id) {
|
||||
auto it = std::remove_if(m_users.user.begin(), m_users.user.end(),
|
||||
[user_id](const User& u) { return u.user_id == user_id; });
|
||||
if (it == m_users.user.end())
|
||||
return false; // not found
|
||||
|
||||
const auto user_dir = EmulatorSettings::GetInstance()->GetHomeDir() / std::to_string(user_id);
|
||||
|
||||
if (std::filesystem::exists(user_dir)) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(user_dir, ec);
|
||||
}
|
||||
|
||||
m_users.user.erase(it, m_users.user.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UserManager::RenameUser(s32 user_id, const std::string& new_name) {
|
||||
// Find user in the internal list
|
||||
for (auto& user : m_users.user) {
|
||||
if (user.user_id == user_id) {
|
||||
if (user.user_name == new_name)
|
||||
return true; // no change
|
||||
|
||||
user.user_name = new_name;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
User* UserManager::GetUserByID(s32 user_id) {
|
||||
for (auto& u : m_users.user) {
|
||||
if (u.user_id == user_id)
|
||||
return &u;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<User>& UserManager::GetAllUsers() const {
|
||||
return m_users.user;
|
||||
}
|
||||
|
||||
std::vector<User> UserManager::CreateDefaultUser() {
|
||||
User default_user;
|
||||
default_user.user_id = 1;
|
||||
default_user.user_color = 0; // BLUE
|
||||
default_user.user_name = "shadPS4";
|
||||
default_user.controller_port = 1;
|
||||
|
||||
const auto user_dir =
|
||||
EmulatorSettings::GetInstance()->GetHomeDir() / std::to_string(default_user.user_id);
|
||||
|
||||
if (!std::filesystem::exists(user_dir)) {
|
||||
std::filesystem::create_directory(user_dir);
|
||||
std::filesystem::create_directory(user_dir / "savedata");
|
||||
std::filesystem::create_directory(user_dir / "trophy");
|
||||
}
|
||||
|
||||
return {default_user};
|
||||
}
|
||||
|
||||
bool UserManager::SetDefaultUser(u32 user_id) {
|
||||
auto it = std::find_if(m_users.user.begin(), m_users.user.end(),
|
||||
[user_id](const User& u) { return u.user_id == user_id; });
|
||||
if (it == m_users.user.end())
|
||||
return false;
|
||||
|
||||
m_users.default_user_id = user_id;
|
||||
SetControllerPort(user_id, 1); // Set default user to port 1
|
||||
return true;
|
||||
}
|
||||
|
||||
void UserManager::SetControllerPort(u32 user_id, int port) {
|
||||
for (auto& u : m_users.user) {
|
||||
if (u.user_id != user_id && u.controller_port == port)
|
||||
u.controller_port = -1;
|
||||
if (u.user_id == user_id)
|
||||
u.controller_port = port;
|
||||
}
|
||||
}
|
||||
44
src/core/user_manager.h
Normal file
44
src/core/user_manager.h
Normal file
@@ -0,0 +1,44 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/types.h"
|
||||
|
||||
struct User {
|
||||
s32 user_id;
|
||||
u32 user_color;
|
||||
std::string user_name;
|
||||
int controller_port; // 1<>4
|
||||
};
|
||||
|
||||
struct Users {
|
||||
int default_user_id = 1;
|
||||
std::vector<User> user;
|
||||
};
|
||||
|
||||
class UserManager {
|
||||
public:
|
||||
UserManager() = default;
|
||||
|
||||
bool AddUser(const User& user);
|
||||
bool RemoveUser(s32 user_id);
|
||||
bool RenameUser(s32 user_id, const std::string& new_name);
|
||||
User* GetUserByID(s32 user_id);
|
||||
const std::vector<User>& GetAllUsers() const;
|
||||
std::vector<User> CreateDefaultUser();
|
||||
bool SetDefaultUser(u32 user_id);
|
||||
void SetControllerPort(u32 user_id, int port);
|
||||
|
||||
Users& GetUsers() {
|
||||
return m_users;
|
||||
}
|
||||
const Users& GetUsers() const {
|
||||
return m_users;
|
||||
}
|
||||
|
||||
private:
|
||||
Users m_users;
|
||||
};
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "common/singleton.h"
|
||||
#include "core/debugger.h"
|
||||
#include "core/devtools/widget/module_list.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/file_format/psf.h"
|
||||
#include "core/file_format/trp.h"
|
||||
#include "core/file_sys/fs.h"
|
||||
@@ -180,7 +181,7 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
|
||||
true);
|
||||
|
||||
// Initialize logging as soon as possible
|
||||
if (!id.empty() && Config::getSeparateLogFilesEnabled()) {
|
||||
if (!id.empty() && EmulatorSettings::GetInstance()->IsSeparateLoggingEnabled()) {
|
||||
Common::Log::Initialize(id + ".log");
|
||||
} else {
|
||||
Common::Log::Initialize();
|
||||
@@ -203,7 +204,7 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
|
||||
LOG_INFO(Config, "Game-specific config exists: {}", has_game_config);
|
||||
|
||||
LOG_INFO(Config, "General LogType: {}", Config::getLogType());
|
||||
LOG_INFO(Config, "General isNeo: {}", Config::isNeoModeConsole());
|
||||
LOG_INFO(Config, "General isNeo: {}", EmulatorSettings::GetInstance()->IsNeo());
|
||||
LOG_INFO(Config, "General isDevKit: {}", Config::isDevKitConsole());
|
||||
LOG_INFO(Config, "General isConnectedToNetwork: {}", Config::getIsConnectedToNetwork());
|
||||
LOG_INFO(Config, "General isPsnSignedIn: {}", Config::getPSNSignedIn());
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <core/emulator_settings.h>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
@@ -28,6 +29,8 @@ int main(int argc, char* argv[]) {
|
||||
IPC::Instance().Init();
|
||||
|
||||
// Load configurations
|
||||
std::shared_ptr<EmulatorSettings> emu_settings = std::make_shared<EmulatorSettings>();
|
||||
EmulatorSettings::SetInstance(emu_settings);
|
||||
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
|
||||
Config::load(user_dir / "config.toml");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user