/** ****************************************************************************** * 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 #include #include #include #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 #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* 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 CommandVar : virtual public ICommandVar { public: CommandVar(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 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 ConfigVar : public CommandVar, virtual public IConfigVar { public: ConfigVar(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 config_value_ = nullptr; std::unique_ptr game_config_value_ = nullptr; void UpdateValue() override; void ResetConfigValueToDefault() override; }; #pragma warning(pop) template const std::string& CommandVar::name() const { return name_; } template const std::string& CommandVar::description() const { return description_; } template void CommandVar::AddToLaunchOptions(cxxopts::Options* options) { options->add_options()(name_, description_, cxxopts::value()); } template <> inline void CommandVar::AddToLaunchOptions( cxxopts::Options* options) { options->add_options()(name_, description_, cxxopts::value()); } template void ConfigVar::AddToLaunchOptions(cxxopts::Options* options) { options->add_options(category_)(this->name_, this->description_, cxxopts::value()); } template <> inline void ConfigVar::AddToLaunchOptions( cxxopts::Options* options) { options->add_options(category_)(this->name_, this->description_, cxxopts::value()); } template void CommandVar::LoadFromLaunchOptions(cxxopts::ParseResult* result) { T value = (*result)[name_].template as(); SetCommandLineValue(value); } template <> inline void CommandVar::LoadFromLaunchOptions( cxxopts::ParseResult* result) { std::string value = (*result)[name_].template as(); SetCommandLineValue(value); } template void ConfigVar::LoadConfigValue(const toml::node* result) { auto value_opt = result->value(); 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(); } config_type_mismatch_warnings->push_back(this->name_); } } template <> inline void ConfigVar::LoadConfigValue( const toml::node* result) { SetConfigValue( xe::utf8::fix_path_separators(result->as_string()->value_or(""))); } template void ConfigVar::LoadGameConfigValue(const toml::node* result) { auto value_opt = result->value(); 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(); } config_type_mismatch_warnings->push_back(this->name_); } } template <> inline void ConfigVar::LoadGameConfigValue( const toml::node* result) { SetGameConfigValue( xe::utf8::fix_path_separators(result->as_string()->value_or(""))); } template CommandVar::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 ConfigVar::ConfigVar(const char* name, T* default_value, const char* description, const char* category, bool is_transient) : CommandVar(name, default_value, description), category_(category), is_transient_(is_transient) {} template void CommandVar::UpdateValue() { if (commandline_value_) return SetValue(*commandline_value_); return SetValue(default_value_); } template void ConfigVar::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 T CommandVar::Convert(std::string val) { return xe::string_util::from_string(val); } template <> inline std::string CommandVar::Convert(std::string val) { return val; } template <> inline std::filesystem::path CommandVar::Convert( std::string val) { return xe::to_path(val); } template <> inline std::string CommandVar::ToString(bool val) { return val ? "true" : "false"; } template <> inline std::string CommandVar::ToString(std::string val) { return toml_internal::EscapeString(val); } template <> inline std::string CommandVar::ToString( std::filesystem::path val) { return toml_internal::EscapeString( xe::utf8::fix_path_separators(xe::path_to_utf8(val), '/')); } template std::string CommandVar::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 void CommandVar::SetValue(T val) { *current_value_ = val; } template const std::string& ConfigVar::category() const { return category_; } template bool ConfigVar::is_transient() const { return is_transient_; } template std::string ConfigVar::config_value() const { if (config_value_) return this->ToString(*config_value_); return this->ToString(this->default_value_); } template const T& ConfigVar::GetTypedConfigValue() const { return config_value_ ? *config_value_ : this->default_value_; } template void CommandVar::SetCommandLineValue(const T val) { commandline_value_ = std::make_unique(val); UpdateValue(); } template void ConfigVar::SetConfigValue(T val) { config_value_ = std::make_unique(val); UpdateValue(); } template void ConfigVar::SetGameConfigValue(T val) { game_config_value_ = std::make_unique(val); UpdateValue(); } template void ConfigVar::OverrideConfigValue(T val) { config_value_ = std::make_unique(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 void ConfigVar::ResetConfigValueToDefault() { SetConfigValue(this->default_value_); } // CVars can be initialized before these, thus initialized on-demand using new. extern std::map* CmdVars; extern std::map* ConfigVars; inline void AddConfigVar(IConfigVar* cv) { if (!ConfigVars) { ConfigVars = new std::map; } ConfigVars->emplace(cv->name(), cv); } inline void AddCommandVar(ICommandVar* cv) { if (!CmdVars) { CmdVars = new std::map; } CmdVars->emplace(cv->name(), cv); } void ParseLaunchArguments(int& argc, char**& argv, const std::string_view positional_help, const std::vector& positional_options); #if XE_PLATFORM_ANDROID void ParseLaunchArgumentsFromAndroidBundle(jobject bundle); #endif // XE_PLATFORM_ANDROID template IConfigVar* define_configvar(const char* name, T* default_value, const char* description, const char* category, bool is_transient) { IConfigVar* cfgvar = new ConfigVar(name, default_value, description, category, is_transient); AddConfigVar(cfgvar); return cfgvar; } template ICommandVar* define_cmdvar(const char* name, T* default_value, const char* description) { ICommandVar* cmdvar = new CommandVar(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*>(&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(2025, 12, 4, 21); 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; } 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* 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 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* config_var_typed = dynamic_cast*>(&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 \ 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_