Files
Xenia-Canary/src/xenia/base/cvar.h
Herman S. 93adb2bb95 [Config] Gracefully handle type mismatches in cvars.
Detect cvar type mismatch and reset to default value, as well
as show message to the user that the cvar from the config has
been updated.
2025-12-01 17:38:34 +01:00

652 lines
25 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CVAR_H_
#define XENIA_CVAR_H_
#include <filesystem>
#include <map>
#include <string>
#include <vector>
#include "third_party/cxxopts/include/cxxopts.hpp"
#include "third_party/fmt/include/fmt/format.h"
#include "third_party/tomlplusplus/include/toml++/toml.hpp"
#include "xenia/base/assert.h"
#include "xenia/base/filesystem.h"
#include "xenia/base/platform.h"
#include "xenia/base/string_util.h"
#if XE_PLATFORM_ANDROID
#include <jni.h>
#endif // XE_PLATFORM_ANDROID
namespace cvar {
namespace toml_internal {
std::string EscapeString(const std::string_view str);
}
// Track config values that had type mismatches during loading
extern std::vector<std::string>* config_type_mismatch_warnings;
class ICommandVar {
public:
virtual ~ICommandVar() = default;
virtual const std::string& name() const = 0;
virtual const std::string& description() const = 0;
virtual void UpdateValue() = 0;
virtual void AddToLaunchOptions(cxxopts::Options* options) = 0;
virtual void LoadFromLaunchOptions(cxxopts::ParseResult* result) = 0;
};
class IConfigVar : virtual public ICommandVar {
public:
virtual const std::string& category() const = 0;
virtual bool is_transient() const = 0;
virtual std::string config_value() const = 0;
virtual void LoadConfigValue(const toml::node* result) = 0;
virtual void LoadGameConfigValue(const toml::node* result) = 0;
virtual void ResetConfigValueToDefault() = 0;
};
template <class T>
class CommandVar : virtual public ICommandVar {
public:
CommandVar<T>(const char* name, T* default_value, const char* description);
const std::string& name() const override;
const std::string& description() const override;
void AddToLaunchOptions(cxxopts::Options* options) override;
void LoadFromLaunchOptions(cxxopts::ParseResult* result) override;
void SetCommandLineValue(T val);
T* current_value() { return current_value_; }
protected:
std::string name_;
T default_value_;
T* current_value_;
std::unique_ptr<T> commandline_value_;
std::string description_;
T Convert(std::string val);
static std::string ToString(T val);
void SetValue(T val);
void UpdateValue() override;
};
#pragma warning(push)
#pragma warning(disable : 4250)
template <class T>
class ConfigVar : public CommandVar<T>, virtual public IConfigVar {
public:
ConfigVar<T>(const char* name, T* default_value, const char* description,
const char* category, bool is_transient);
std::string config_value() const override;
const T& GetTypedConfigValue() const;
const std::string& category() const override;
bool is_transient() const override;
void AddToLaunchOptions(cxxopts::Options* options) override;
void LoadConfigValue(const toml::node* result) override;
void LoadGameConfigValue(const toml::node* result) override;
void SetConfigValue(T val);
void SetGameConfigValue(T val);
// Changes the actual value used to the one specified, and also makes it the
// one that will be stored when the global config is written next time. After
// overriding, however, the next game config loaded may still change it.
void OverrideConfigValue(T val);
private:
std::string category_;
bool is_transient_;
std::unique_ptr<T> config_value_ = nullptr;
std::unique_ptr<T> game_config_value_ = nullptr;
void UpdateValue() override;
void ResetConfigValueToDefault() override;
};
#pragma warning(pop)
template <class T>
const std::string& CommandVar<T>::name() const {
return name_;
}
template <class T>
const std::string& CommandVar<T>::description() const {
return description_;
}
template <class T>
void CommandVar<T>::AddToLaunchOptions(cxxopts::Options* options) {
options->add_options()(name_, description_, cxxopts::value<T>());
}
template <>
inline void CommandVar<std::filesystem::path>::AddToLaunchOptions(
cxxopts::Options* options) {
options->add_options()(name_, description_, cxxopts::value<std::string>());
}
template <class T>
void ConfigVar<T>::AddToLaunchOptions(cxxopts::Options* options) {
options->add_options(category_)(this->name_, this->description_,
cxxopts::value<T>());
}
template <>
inline void ConfigVar<std::filesystem::path>::AddToLaunchOptions(
cxxopts::Options* options) {
options->add_options(category_)(this->name_, this->description_,
cxxopts::value<std::string>());
}
template <class T>
void CommandVar<T>::LoadFromLaunchOptions(cxxopts::ParseResult* result) {
T value = (*result)[name_].template as<T>();
SetCommandLineValue(value);
}
template <>
inline void CommandVar<std::filesystem::path>::LoadFromLaunchOptions(
cxxopts::ParseResult* result) {
std::string value = (*result)[name_].template as<std::string>();
SetCommandLineValue(value);
}
template <class T>
void ConfigVar<T>::LoadConfigValue(const toml::node* result) {
auto value_opt = result->value<T>();
if (value_opt) {
SetConfigValue(value_opt.value());
} else {
// Type mismatch - track for warning
if (!config_type_mismatch_warnings) {
config_type_mismatch_warnings = new std::vector<std::string>();
}
config_type_mismatch_warnings->push_back(this->name_);
}
}
template <>
inline void ConfigVar<std::filesystem::path>::LoadConfigValue(
const toml::node* result) {
SetConfigValue(
xe::utf8::fix_path_separators(result->as_string()->value_or("")));
}
template <class T>
void ConfigVar<T>::LoadGameConfigValue(const toml::node* result) {
auto value_opt = result->value<T>();
if (value_opt) {
SetGameConfigValue(value_opt.value());
} else {
// Type mismatch - track for warning
if (!config_type_mismatch_warnings) {
config_type_mismatch_warnings = new std::vector<std::string>();
}
config_type_mismatch_warnings->push_back(this->name_);
}
}
template <>
inline void ConfigVar<std::filesystem::path>::LoadGameConfigValue(
const toml::node* result) {
SetGameConfigValue(
xe::utf8::fix_path_separators(result->as_string()->value_or("")));
}
template <class T>
CommandVar<T>::CommandVar(const char* name, T* default_value,
const char* description)
: name_(name),
default_value_(*default_value),
current_value_(default_value),
commandline_value_(),
description_(description) {}
template <class T>
ConfigVar<T>::ConfigVar(const char* name, T* default_value,
const char* description, const char* category,
bool is_transient)
: CommandVar<T>(name, default_value, description),
category_(category),
is_transient_(is_transient) {}
template <class T>
void CommandVar<T>::UpdateValue() {
if (commandline_value_) return SetValue(*commandline_value_);
return SetValue(default_value_);
}
template <class T>
void ConfigVar<T>::UpdateValue() {
if (this->commandline_value_) {
return this->SetValue(*this->commandline_value_);
}
if (game_config_value_) return this->SetValue(*game_config_value_);
if (config_value_) return this->SetValue(*config_value_);
return this->SetValue(this->default_value_);
}
template <class T>
T CommandVar<T>::Convert(std::string val) {
return xe::string_util::from_string<T>(val);
}
template <>
inline std::string CommandVar<std::string>::Convert(std::string val) {
return val;
}
template <>
inline std::filesystem::path CommandVar<std::filesystem::path>::Convert(
std::string val) {
return xe::to_path(val);
}
template <>
inline std::string CommandVar<bool>::ToString(bool val) {
return val ? "true" : "false";
}
template <>
inline std::string CommandVar<std::string>::ToString(std::string val) {
return toml_internal::EscapeString(val);
}
template <>
inline std::string CommandVar<std::filesystem::path>::ToString(
std::filesystem::path val) {
return toml_internal::EscapeString(
xe::utf8::fix_path_separators(xe::path_to_utf8(val), '/'));
}
template <class T>
std::string CommandVar<T>::ToString(T val) {
// Use fmt::format instead of std::to_string for locale-neutral formatting of
// floats, always with a period rather than a comma, which is treated as an
// unidentified trailing character by cpptoml.
return fmt::format("{}", val);
}
template <class T>
void CommandVar<T>::SetValue(T val) {
*current_value_ = val;
}
template <class T>
const std::string& ConfigVar<T>::category() const {
return category_;
}
template <class T>
bool ConfigVar<T>::is_transient() const {
return is_transient_;
}
template <class T>
std::string ConfigVar<T>::config_value() const {
if (config_value_) return this->ToString(*config_value_);
return this->ToString(this->default_value_);
}
template <class T>
const T& ConfigVar<T>::GetTypedConfigValue() const {
return config_value_ ? *config_value_ : this->default_value_;
}
template <class T>
void CommandVar<T>::SetCommandLineValue(const T val) {
commandline_value_ = std::make_unique<T>(val);
UpdateValue();
}
template <class T>
void ConfigVar<T>::SetConfigValue(T val) {
config_value_ = std::make_unique<T>(val);
UpdateValue();
}
template <class T>
void ConfigVar<T>::SetGameConfigValue(T val) {
game_config_value_ = std::make_unique<T>(val);
UpdateValue();
}
template <class T>
void ConfigVar<T>::OverrideConfigValue(T val) {
config_value_ = std::make_unique<T>(val);
// The user explicitly changes the value at runtime and wants it to take
// effect immediately. Drop everything with a higher priority. The next game
// config load, however, may still change it.
game_config_value_.reset();
this->commandline_value_.reset();
UpdateValue();
}
template <class T>
void ConfigVar<T>::ResetConfigValueToDefault() {
SetConfigValue(this->default_value_);
}
// CVars can be initialized before these, thus initialized on-demand using new.
extern std::map<std::string, ICommandVar*>* CmdVars;
extern std::map<std::string, IConfigVar*>* ConfigVars;
inline void AddConfigVar(IConfigVar* cv) {
if (!ConfigVars) {
ConfigVars = new std::map<std::string, IConfigVar*>;
}
ConfigVars->emplace(cv->name(), cv);
}
inline void AddCommandVar(ICommandVar* cv) {
if (!CmdVars) {
CmdVars = new std::map<std::string, ICommandVar*>;
}
CmdVars->emplace(cv->name(), cv);
}
void ParseLaunchArguments(int& argc, char**& argv,
const std::string_view positional_help,
const std::vector<std::string>& positional_options);
#if XE_PLATFORM_ANDROID
void ParseLaunchArgumentsFromAndroidBundle(jobject bundle);
#endif // XE_PLATFORM_ANDROID
template <typename T>
IConfigVar* define_configvar(const char* name, T* default_value,
const char* description, const char* category,
bool is_transient) {
IConfigVar* cfgvar = new ConfigVar<T>(name, default_value, description,
category, is_transient);
AddConfigVar(cfgvar);
return cfgvar;
}
template <typename T>
ICommandVar* define_cmdvar(const char* name, T* default_value,
const char* description) {
ICommandVar* cmdvar = new CommandVar<T>(name, default_value, description);
AddCommandVar(cmdvar);
return cmdvar;
}
#define DEFINE_bool(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, bool)
#define DEFINE_int32(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, int32_t)
#define DEFINE_uint32(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, uint32_t)
#define DEFINE_uint64(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, uint64_t)
#define DEFINE_int64(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, int64_t)
#define DEFINE_double(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, double)
#define DEFINE_string(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, std::string)
#define DEFINE_path(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, false, \
std::filesystem::path)
#define DEFINE_transient_bool(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, true, bool)
#define DEFINE_transient_string(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, true, std::string)
#define DEFINE_transient_path(name, default_value, description, category) \
DEFINE_CVar(name, default_value, description, category, true, \
std::filesystem::path)
#define DEFINE_CVar(name, default_value, description, category, is_transient, \
type) \
namespace cvars { \
type name = default_value; \
} \
namespace cv { \
static cvar::IConfigVar* const cv_##name = cvar::define_configvar( \
#name, &cvars::name, description, category, is_transient); \
}
// CmdVars can only be strings for now, we don't need any others
#define CmdVar(name, default_value, description) \
namespace cvars { \
std::string name = default_value; \
} \
namespace cv { \
static cvar::ICommandVar* const cv_##name = \
cvar::define_cmdvar(#name, &cvars::name, description); \
}
#define DECLARE_bool(name) DECLARE_CVar(name, bool)
#define DECLARE_int32(name) DECLARE_CVar(name, int32_t)
#define DECLARE_uint32(name) DECLARE_CVar(name, uint32_t)
#define DECLARE_uint64(name) DECLARE_CVar(name, uint64_t)
#define DECLARE_int64(name) DECLARE_CVar(name, int64_t)
#define DECLARE_double(name) DECLARE_CVar(name, double)
#define DECLARE_string(name) DECLARE_CVar(name, std::string)
#define DECLARE_path(name) DECLARE_CVar(name, std::filesystem::path)
#define DECLARE_CVar(name, type) \
namespace cvars { \
extern type name; \
}
#define ACCESS_CVar(name) (*cv::cv_##name)
// dynamic_cast is needed because of virtual inheritance.
#define OVERRIDE_CVar(name, type, value) \
dynamic_cast<cvar::ConfigVar<type>*>(&ACCESS_CVar(name)) \
->OverrideConfigValue(value);
#define OVERRIDE_bool(name, value) OVERRIDE_CVar(name, bool, value)
#define OVERRIDE_int32(name, value) OVERRIDE_CVar(name, int32_t, value)
#define OVERRIDE_uint32(name, value) OVERRIDE_CVar(name, uint32_t, value)
#define OVERRIDE_uint64(name, value) OVERRIDE_CVar(name, uint64_t, value)
#define OVERRIDE_double(name, value) OVERRIDE_CVar(name, double, value)
#define OVERRIDE_string(name, value) OVERRIDE_CVar(name, std::string, value)
#define OVERRIDE_path(name, value) \
OVERRIDE_CVar(name, std::filesystem::path, value)
// Interface for changing the default value of a variable with auto-upgrading of
// users' configs (to distinguish between a leftover old default and an explicit
// override), without having to rename the variable.
//
// Two types of updates are supported:
// - Changing the value of the variable (UPDATE_from_type) from an explicitly
// specified previous default value to a new one, but keeping the
// user-specified value if it was not the default, and thus explicitly
// overridden.
// - Changing the meaning / domain of the variable (UPDATE_from_any), when
// previous user-specified overrides also stop making sense. Config variable
// type changes are also considered this type of updates (though
// UPDATE_from_type, if the new type doesn't match the previous one, is also
// safe to use - it behaves like UPDATE_from_any in this case).
//
// Rules of using UPDATE_:
// - Do not remove previous UPDATE_ entries (both typed and from-any) if you're
// adding a new UPDATE_from_type.
// This ensures that if the default was changed from 1 to 2 and then to 3,
// both users who last launched Xenia when it was 1 and when it was 2 receive
// the update (however, those who have explicitly changed it from 2 to 1 when
// 2 was the default will have it kept at 1).
// It's safe to remove the history before a new UPDATE_from_any, however.
// - The date should preferably be in UTC+0 timezone.
// - No other pull recent pull requests should have the same date (since builds
// are made after every commit).
// - IConfigVarUpdate::kLastCommittedUpdateDate must be updated - see the
// comment near its declaration.
constexpr uint32_t MakeConfigVarUpdateDate(uint32_t year, uint32_t month,
uint32_t day, uint32_t utc_hour) {
// Written to the config as a decimal number - pack as decimal for user
// readability.
// Using 31 bits in the 3rd millennium already - don't add more digits.
return utc_hour + day * 100 + month * 10000 + year * 1000000;
}
class IConfigVarUpdate {
public:
// This global highest version constant is used to ensure that version (which
// is stored as one value for the whole config file) is monotonically
// increased when commits - primarily pull requests - are pushed to the main
// branch.
//
// This is to prevent the following situation:
// - Pull request #1 created on day 1.
// - Pull request #2 created on day 2.
// - Pull request #2 from day 2 merged on day 3.
// - User launches the latest version on day 4.
// CVar default changes from PR #2 (day 2) applied because the user's config
// version is day 0, which is < 2.
// User's config has day 2 version now.
// - Pull request #1 from day 1 merged on day 5.
// - User launches the latest version on day 5.
// CVar default changes from PR #1 (day 1) IGNORED because the user's config
// version is day 2, which is >= 1.
//
// If this constant is not updated, static_assert will be triggered for a new
// DEFINE_, requiring this constant to be raised. But changing this will
// result in merge conflicts in all other pull requests also changing cvar
// defaults - before they're merged, they will need to be updated, which will
// ensure monotonic growth of the versions of all cvars on the main branch. In
// the example above, PR #1 will need to be updated before it's merged.
//
// If you've encountered a merge conflict here in your pull request:
// 1) Update any UPDATE_s you've added in the pull request to the current
// date.
// 2) Change this value to the same date.
// If you're reviewing a pull request with a change here, check if 1) has been
// done by the submitter before merging.
static constexpr uint32_t kLastCommittedUpdateDate =
MakeConfigVarUpdateDate(2024, 9, 23, 9);
virtual ~IConfigVarUpdate() = default;
virtual void Apply() const = 0;
static void ApplyUpdates(uint32_t config_date) {
if (!updates_) {
return;
}
auto it_end = updates_->end();
for (auto it = updates_->upper_bound(config_date); it != it_end; ++it) {
it->second->Apply();
}
}
// More reliable than kLastCommittedUpdateDate for actual usage
// (kLastCommittedUpdateDate is just a pull request merge order guard), though
// usually should be the same, but kLastCommittedUpdateDate may not include
// removal of cvars.
static uint32_t GetLastUpdateDate() {
return (updates_ && !updates_->empty()) ? updates_->crbegin()->first : 0;
}
protected:
IConfigVarUpdate(IConfigVar* const& config_var, uint32_t year, uint32_t month,
uint32_t day, uint32_t utc_hour)
: config_var_(config_var) {
if (!updates_) {
updates_ = new std::multimap<uint32_t, const IConfigVarUpdate*>;
}
updates_->emplace(MakeConfigVarUpdateDate(year, month, day, utc_hour),
this);
}
IConfigVar& config_var() const {
assert_not_null(config_var_);
return *config_var_;
}
private:
// Reference to pointer to loosen initialization order requirements.
IConfigVar* const& config_var_;
// Updates can be initialized before these, thus initialized on demand using
// `new`.
static std::multimap<uint32_t, const IConfigVarUpdate*>* updates_;
};
class ConfigVarUpdateFromAny : public IConfigVarUpdate {
public:
ConfigVarUpdateFromAny(IConfigVar* const& config_var, uint32_t year,
uint32_t month, uint32_t day, uint32_t utc_hour)
: IConfigVarUpdate(config_var, year, month, day, utc_hour) {}
void Apply() const override { config_var().ResetConfigValueToDefault(); }
};
template <typename T>
class ConfigVarUpdate : public IConfigVarUpdate {
public:
ConfigVarUpdate(IConfigVar* const& config_var, uint32_t year, uint32_t month,
uint32_t day, uint32_t utc_hour, const T& old_default_value)
: IConfigVarUpdate(config_var, year, month, day, utc_hour),
old_default_value_(old_default_value) {}
void Apply() const override {
IConfigVar& config_var_untyped = config_var();
ConfigVar<T>* config_var_typed =
dynamic_cast<ConfigVar<T>*>(&config_var_untyped);
// Update only from the previous default value if the same type,
// unconditionally reset if the type has been changed.
if (!config_var_typed ||
config_var_typed->GetTypedConfigValue() == old_default_value_) {
config_var_untyped.ResetConfigValueToDefault();
}
}
private:
T old_default_value_;
};
#define UPDATE_from_any(name, year, month, day, utc_hour) \
static_assert( \
cvar::MakeConfigVarUpdateDate(year, month, day, utc_hour) <= \
cvar::IConfigVarUpdate::kLastCommittedUpdateDate, \
"A new config variable default value update was added - raise " \
"cvar::IConfigVarUpdate::kLastCommittedUpdateDate to the same date in " \
"base/cvar.h to ensure coherence between different pull requests " \
"updating config variable defaults."); \
namespace cv { \
static const cvar::ConfigVarUpdateFromAny \
update_##name_##year_##month_##day_##utc_hour(cv_##name, year, month, \
day, utc_hour); \
}
#define UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, type) \
static_assert( \
cvar::MakeConfigVarUpdateDate(year, month, day, utc_hour) <= \
cvar::IConfigVarUpdate::kLastCommittedUpdateDate, \
"A new config variable default value update was added - raise " \
"cvar::IConfigVarUpdate::kLastCommittedUpdateDate to the same date in " \
"base/cvar.h to ensure coherence between different pull requests " \
"updating config variable defaults."); \
namespace cv { \
static const cvar::ConfigVarUpdate<type> \
update_##name_##year_##month_##day_##utc_hour(cv_##name, year, month, \
day, utc_hour, \
old_default_value); \
}
#define UPDATE_from_bool(name, year, month, day, utc_hour, old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, bool)
#define UPDATE_from_int32(name, year, month, day, utc_hour, old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, int32_t)
#define UPDATE_from_uint32(name, year, month, day, utc_hour, \
old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, uint32_t)
#define UPDATE_from_uint64(name, year, month, day, utc_hour, \
old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, uint64_t)
#define UPDATE_from_double(name, year, month, day, utc_hour, \
old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, double)
#define UPDATE_from_string(name, year, month, day, utc_hour, \
old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, std::string)
#define UPDATE_from_path(name, year, month, day, utc_hour, old_default_value) \
UPDATE_CVar(name, year, month, day, utc_hour, old_default_value, \
std::filesystem::path)
} // namespace cvar
#endif // XENIA_CVAR_H_