shadPS4/src/common/config.cpp
2026-01-09 20:59:46 +02:00

486 lines
14 KiB
C++

// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <optional>
#include <string>
#include <fmt/core.h>
#include <fmt/xchar.h> // for wstring support
#include <toml.hpp>
#include "common/assert.h"
#include "common/config.h"
#include "common/logging/formatter.h"
#include "common/path_util.h"
#include "common/scm_rev.h"
using std::nullopt;
using std::optional;
using std::string;
namespace toml {
template <typename TC, typename K>
std::filesystem::path find_fs_path_or(const basic_value<TC>& v, const K& ky,
std::filesystem::path opt) {
try {
auto str = find<string>(v, ky);
if (str.empty()) {
return opt;
}
std::u8string u8str{(char8_t*)&str.front(), (char8_t*)&str.back() + 1};
return std::filesystem::path{u8str};
} catch (...) {
return opt;
}
}
// why is it so hard to avoid exceptions with this library
template <typename T>
std::optional<T> get_optional(const toml::value& v, const std::string& key) {
if (!v.is_table())
return std::nullopt;
const auto& tbl = v.as_table();
auto it = tbl.find(key);
if (it == tbl.end())
return std::nullopt;
if constexpr (std::is_same_v<T, int>) {
if (it->second.is_integer()) {
return static_cast<s32>(toml::get<int>(it->second));
}
} else if constexpr (std::is_same_v<T, unsigned int>) {
if (it->second.is_integer()) {
return static_cast<u32>(toml::get<unsigned int>(it->second));
}
} else if constexpr (std::is_same_v<T, double>) {
if (it->second.is_floating()) {
return toml::get<T>(it->second);
}
} else if constexpr (std::is_same_v<T, std::string>) {
if (it->second.is_string()) {
return toml::get<T>(it->second);
}
} else if constexpr (std::is_same_v<T, bool>) {
if (it->second.is_boolean()) {
return toml::get<T>(it->second);
}
} else if constexpr (std::is_same_v<T, std::array<string, 4>>) {
if (it->second.is_array()) {
return toml::get<T>(it->second);
}
} else {
static_assert([] { return false; }(), "Unsupported type in get_optional<T>");
}
return std::nullopt;
}
} // namespace toml
namespace Config {
ConfigMode config_mode = ConfigMode::Default;
void setConfigMode(ConfigMode mode) {
config_mode = mode;
}
template <typename T>
class ConfigEntry {
public:
const T default_value;
T base_value;
optional<T> game_specific_value;
ConfigEntry(const T& t = T()) : default_value(t), base_value(t), game_specific_value(nullopt) {}
ConfigEntry operator=(const T& t) {
base_value = t;
return *this;
}
const T get() const {
switch (config_mode) {
case ConfigMode::Default:
return game_specific_value.value_or(base_value);
case ConfigMode::Global:
return base_value;
case ConfigMode::Clean:
return default_value;
default:
UNREACHABLE();
}
}
void setFromToml(const toml::value& v, const std::string& key, bool is_game_specific = false) {
if (is_game_specific) {
game_specific_value = toml::get_optional<T>(v, key);
} else {
base_value = toml::get_optional<T>(v, key).value_or(base_value);
}
}
void set(const T value, bool is_game_specific = false) {
is_game_specific ? game_specific_value = value : base_value = value;
}
void setDefault(bool is_game_specific = false) {
is_game_specific ? game_specific_value = default_value : base_value = default_value;
}
void setTomlValue(toml::ordered_value& data, const std::string& header, const std::string& key,
bool is_game_specific = false) {
if (is_game_specific) {
data[header][key] = game_specific_value.value_or(base_value);
game_specific_value = std::nullopt;
} else {
data[header][key] = base_value;
}
}
// operator T() {
// return get();
// }
};
// General
static ConfigEntry<std::array<std::string, 4>> userNames({
"shadPS4",
"shadps4-2",
"shadPS4-3",
"shadPS4-4",
});
static ConfigEntry<bool> useUnifiedInputConfig(true);
// Keys
static string trophyKey = "";
// Config version, used to determine if a user's config file is outdated.
static string config_version = Common::g_scm_rev;
// These entries aren't stored in the config
static bool overrideControllerColor = false;
static int controllerCustomColorRGB[3] = {0, 0, 255};
bool GetUseUnifiedInputConfig() {
return useUnifiedInputConfig.get();
}
void SetUseUnifiedInputConfig(bool use) {
useUnifiedInputConfig.base_value = use;
}
bool GetOverrideControllerColor() {
return overrideControllerColor;
}
void SetOverrideControllerColor(bool enable) {
overrideControllerColor = enable;
}
int* GetControllerCustomColor() {
return controllerCustomColorRGB;
}
void SetControllerCustomColor(int r, int b, int g) {
controllerCustomColorRGB[0] = r;
controllerCustomColorRGB[1] = b;
controllerCustomColorRGB[2] = g;
}
string getTrophyKey() {
return trophyKey;
}
void setTrophyKey(string key) {
trophyKey = key;
}
void setUserName(int id, string name) {
auto temp = userNames.get();
temp[id] = name;
userNames.set(temp);
}
std::array<string, 4> const getUserNames() {
return userNames.get();
}
std::string getUserName(int id) {
return userNames.get()[id];
}
void load(const std::filesystem::path& path, bool is_game_specific) {
// If the configuration file does not exist, create it and return, unless it is game specific
std::error_code error;
if (!std::filesystem::exists(path, error)) {
if (!is_game_specific) {
save(path);
}
return;
}
toml::value data;
try {
std::ifstream ifs;
ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
ifs.open(path, std::ios_base::binary);
data = toml::parse(ifs, string{fmt::UTF(path.filename().u8string()).data});
} catch (std::exception& ex) {
fmt::print("Got exception trying to load config file. Exception: {}\n", ex.what());
return;
}
if (data.contains("General")) {
const toml::value& general = data.at("General");
userNames.setFromToml(general, "userNames", is_game_specific);
}
if (data.contains("Input")) {
const toml::value& input = data.at("Input");
useUnifiedInputConfig.setFromToml(input, "useUnifiedInputConfig", is_game_specific);
}
string current_version = {};
if (data.contains("Debug")) {
const toml::value& debug = data.at("Debug");
current_version = toml::find_or<std::string>(debug, "ConfigVersion", current_version);
}
if (data.contains("Keys")) {
const toml::value& keys = data.at("Keys");
trophyKey = toml::find_or<string>(keys, "TrophyKey", trophyKey);
}
// Run save after loading to generate any missing fields with default values.
if (config_version != current_version && !is_game_specific) {
save(path);
}
}
void sortTomlSections(toml::ordered_value& data) {
toml::ordered_value ordered_data;
std::vector<string> section_order = {"General", "Input", "Audio", "GPU", "Vulkan",
"Debug", "Keys", "GUI", "Settings"};
for (const auto& section : section_order) {
if (data.contains(section)) {
std::vector<string> keys;
for (const auto& item : data.at(section).as_table()) {
keys.push_back(item.first);
}
std::sort(keys.begin(), keys.end(), [](const string& a, const string& b) {
return std::lexicographical_compare(
a.begin(), a.end(), b.begin(), b.end(), [](char a_char, char b_char) {
return std::tolower(a_char) < std::tolower(b_char);
});
});
toml::ordered_value ordered_section;
for (const auto& key : keys) {
ordered_section[key] = data.at(section).at(key);
}
ordered_data[section] = ordered_section;
}
}
data = ordered_data;
}
void save(const std::filesystem::path& path, bool is_game_specific) {
toml::ordered_value data;
std::error_code error;
if (std::filesystem::exists(path, error)) {
try {
std::ifstream ifs;
ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
ifs.open(path, std::ios_base::binary);
data = toml::parse<toml::ordered_type_config>(
ifs, string{fmt::UTF(path.filename().u8string()).data});
} catch (const std::exception& ex) {
fmt::print("Exception trying to parse config file. Exception: {}\n", ex.what());
return;
}
} else {
if (error) {
fmt::print("Filesystem error: {}\n", error.message());
}
fmt::print("Saving new configuration file {}\n", fmt::UTF(path.u8string()));
}
// Entries saved by the game-specific settings GUI
userNames.setTomlValue(data, "General", "userNames", is_game_specific);
if (!is_game_specific) {
// Non game-specific entries
data["Debug"]["ConfigVersion"] = config_version;
data["Keys"]["TrophyKey"] = trophyKey;
// Do not save these entries in the game-specific dialog since they are not in the GUI
data["Input"]["useUnifiedInputConfig"] = useUnifiedInputConfig.base_value;
}
// Sorting of TOML sections
sortTomlSections(data);
std::ofstream file(path, std::ios::binary);
file << data;
file.close();
}
void setDefaultValues(bool is_game_specific) {
// Entries with game-specific settings that are in the game-specific setings GUI but not in
// the global settings GUI
// Entries with game-specific settings that are in both the game-specific and global GUI
// GS - General
userNames.setDefault(is_game_specific);
// All other entries
if (!is_game_specific) {
// Input
useUnifiedInputConfig.base_value = true;
controllerCustomColorRGB[0] = 0;
controllerCustomColorRGB[1] = 0;
controllerCustomColorRGB[2] = 255;
}
}
constexpr std::string_view GetDefaultGlobalConfig() {
return R"(# Anything put here will be loaded for all games,
# alongside the game's config or default.ini depending on your preference.
hotkey_renderdoc_capture = f12
hotkey_fullscreen = f11
hotkey_show_fps = f10
hotkey_pause = f9
hotkey_reload_inputs = f8
hotkey_toggle_mouse_to_joystick = f7
hotkey_toggle_mouse_to_gyro = f6
hotkey_add_virtual_user = f5
hotkey_remove_virtual_user = f4
hotkey_toggle_mouse_to_touchpad = delete
hotkey_quit = lctrl, lshift, end
)";
}
constexpr std::string_view GetDefaultInputConfig() {
return R"(#Feeling lost? Check out the Help section!
# Keyboard bindings
triangle = kp8
circle = kp6
cross = kp2
square = kp4
# Alternatives for users without a keypad
triangle = c
circle = b
cross = n
square = v
l1 = q
r1 = u
l2 = e
r2 = o
l3 = x
r3 = m
options = enter
touchpad_center = space
pad_up = up
pad_down = down
pad_left = left
pad_right = right
axis_left_x_minus = a
axis_left_x_plus = d
axis_left_y_minus = w
axis_left_y_plus = s
axis_right_x_minus = j
axis_right_x_plus = l
axis_right_y_minus = i
axis_right_y_plus = k
# Controller bindings
triangle = triangle
cross = cross
square = square
circle = circle
l1 = l1
l2 = l2
l3 = l3
r1 = r1
r2 = r2
r3 = r3
options = options
touchpad_center = back
pad_up = pad_up
pad_down = pad_down
pad_left = pad_left
pad_right = pad_right
axis_left_x = axis_left_x
axis_left_y = axis_left_y
axis_right_x = axis_right_x
axis_right_y = axis_right_y
# Range of deadzones: 1 (almost none) to 127 (max)
analog_deadzone = leftjoystick, 2, 127
analog_deadzone = rightjoystick, 2, 127
override_controller_color = false, 0, 0, 255
)";
}
std::filesystem::path GetFoolproofInputConfigFile(const string& game_id) {
// Read configuration file of the game, and if it doesn't exist, generate it from default
// If that doesn't exist either, generate that from getDefaultConfig() and try again
// If even the folder is missing, we start with that.
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "input_config";
const auto config_file = config_dir / (game_id + ".ini");
const auto default_config_file = config_dir / "default.ini";
// Ensure the config directory exists
if (!std::filesystem::exists(config_dir)) {
std::filesystem::create_directories(config_dir);
}
// Check if the default config exists
if (!std::filesystem::exists(default_config_file)) {
// If the default config is also missing, create it from getDefaultConfig()
const auto default_config = GetDefaultInputConfig();
std::ofstream default_config_stream(default_config_file);
if (default_config_stream) {
default_config_stream << default_config;
}
}
// if empty, we only need to execute the function up until this point
if (game_id.empty()) {
return default_config_file;
}
// Create global config if it doesn't exist yet
if (game_id == "global" && !std::filesystem::exists(config_file)) {
if (!std::filesystem::exists(config_file)) {
const auto global_config = GetDefaultGlobalConfig();
std::ofstream global_config_stream(config_file);
if (global_config_stream) {
global_config_stream << global_config;
}
}
}
// If game-specific config doesn't exist, create it from the default config
if (!std::filesystem::exists(config_file)) {
std::filesystem::copy(default_config_file, config_file);
}
return config_file;
}
} // namespace Config