diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index bcc9152d9..a8ce39765 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -81,7 +81,7 @@ int xenia_main(const std::vector& args) { storage_root = std::filesystem::absolute(storage_root); XELOGI("Storage root: {}", xe::path_to_utf8(storage_root)); - config::SetupConfig(storage_root); + Config::Instance().SetupConfig(storage_root); int argc = 1; char* argv[] = {"xenia", nullptr}; diff --git a/src/xenia/base/cvar.cc b/src/xenia/base/cvar.cc index b8fec6a71..ddcd2c534 100644 --- a/src/xenia/base/cvar.cc +++ b/src/xenia/base/cvar.cc @@ -18,70 +18,9 @@ namespace utfcpp = utf8; using u8_citer = utfcpp::iterator; +namespace xe { namespace cvar { -cxxopts::Options options("xenia", "Xbox 360 Emulator"); -std::map* CmdVars; -std::map* ConfigVars; - -void PrintHelpAndExit() { - std::cout << options.help({""}) << std::endl; - std::cout << "For the full list of command line arguments, see xenia.cfg." - << std::endl; - exit(0); -} - -void ParseLaunchArguments(int& argc, char**& argv, - const std::string_view positional_help, - const std::vector& positional_options) { - options.add_options()("help", "Prints help and exit."); - - if (!CmdVars) { - CmdVars = new std::map(); - } - - if (!ConfigVars) { - ConfigVars = new std::map(); - } - - for (auto& it : *CmdVars) { - auto cmdVar = it.second; - cmdVar->AddToLaunchOptions(&options); - } - - for (const auto& it : *ConfigVars) { - auto configVar = it.second; - configVar->AddToLaunchOptions(&options); - } - - try { - options.positional_help(std::string(positional_help)); - options.parse_positional(positional_options); - - auto result = options.parse(argc, argv); - if (result.count("help")) { - PrintHelpAndExit(); - } - - for (auto& it : *CmdVars) { - auto cmdVar = static_cast(it.second); - if (result.count(cmdVar->name())) { - cmdVar->LoadFromLaunchOptions(&result); - } - } - - for (auto& it : *ConfigVars) { - auto configVar = static_cast(it.second); - if (result.count(configVar->name())) { - configVar->LoadFromLaunchOptions(&result); - } - } - } catch (const cxxopts::OptionException& e) { - std::cout << e.what() << std::endl; - PrintHelpAndExit(); - } -} - namespace toml { std::string EscapeBasicString(const std::string_view view) { @@ -199,3 +138,4 @@ std::string EscapeString(const std::string_view view) { } // namespace toml } // namespace cvar +} // namespace xe diff --git a/src/xenia/base/cvar.h b/src/xenia/base/cvar.h index d7ed12b86..d260b7151 100644 --- a/src/xenia/base/cvar.h +++ b/src/xenia/base/cvar.h @@ -17,13 +17,16 @@ #include "cpptoml/include/cpptoml.h" #include "cxxopts/include/cxxopts.hpp" + #include "xenia/base/filesystem.h" #include "xenia/base/string_util.h" +#include "xenia/config.h" +namespace xe { namespace cvar { namespace toml { -std::string EscapeString(const std::string_view str); +std::string EscapeString(const std::string_view view); } class ICommandVar { @@ -40,6 +43,7 @@ class IConfigVar : virtual public ICommandVar { public: virtual const std::string& category() const = 0; virtual bool is_transient() const = 0; + virtual bool requires_restart() const = 0; virtual std::string config_value() const = 0; virtual void LoadConfigValue(std::shared_ptr result) = 0; virtual void LoadGameConfigValue(std::shared_ptr result) = 0; @@ -48,285 +52,176 @@ class IConfigVar : virtual public ICommandVar { 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; + CommandVar(std::string_view name, T* default_value, + std::string_view description); + + const std::string& name() const override { return name_; } + const std::string& description() const override { return description_; } + void AddToLaunchOptions(cxxopts::Options* options) override; void LoadFromLaunchOptions(cxxopts::ParseResult* result) override; + T* current_value() { return current_value_; } + T* command_line_value() { return commandline_value_.get(); } + const T& default_value() const { return default_value_; } protected: + void set_current_value(const T& val) { *current_value_ = val; } + + void set_command_line_value(const T& val) { + commandline_value_ = std::make_unique(val); + } + + T Convert(std::string_view val) const { + return xe::string_util::from_string(val); + } + + static std::string ToString(const T& val) { return std::to_string(val); } + void UpdateValue() override; + + private: 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 SetCommandLineValue(T val); - void UpdateValue() override; }; #pragma warning(push) #pragma warning(disable : 4250) + template -class ConfigVar : public CommandVar, virtual public IConfigVar { +class ConfigVar final : 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 std::string& category() const override; - bool is_transient() const override; + ConfigVar(std::string_view name, T* default_value, + std::string_view description, std::string_view category, + bool is_transient, bool requires_restart); + + std::string config_value() const override { + if (config_value_) return this->ToString(*config_value_); + return this->ToString(this->default_value()); + } + + const std::string& category() const override { return category_; } + bool is_transient() const override { return is_transient_; } + bool requires_restart() const override { return requires_restart_; } + + void set_config_value(const T& val) { + config_value_ = std::make_unique(val); + UpdateValue(); + } + + void set_game_config_value(const T& val) { + game_config_value_ = std::make_unique(val); + UpdateValue(); + } + void AddToLaunchOptions(cxxopts::Options* options) override; void LoadConfigValue(std::shared_ptr result) override; void LoadGameConfigValue(std::shared_ptr result) override; - void SetConfigValue(T val); - void SetGameConfigValue(T val); private: + void UpdateValue() override; + std::string category_; bool is_transient_; + bool requires_restart_; std::unique_ptr config_value_ = nullptr; std::unique_ptr game_config_value_ = nullptr; - void UpdateValue() 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(std::shared_ptr result) { - SetConfigValue(*cpptoml::get_impl(result)); -} -template <> -inline void ConfigVar::LoadConfigValue( - std::shared_ptr result) { - SetConfigValue( - xe::utf8::fix_path_separators(*cpptoml::get_impl(result))); -} -template -void ConfigVar::LoadGameConfigValue(std::shared_ptr result) { - SetGameConfigValue(*cpptoml::get_impl(result)); -} -template <> -inline void ConfigVar::LoadGameConfigValue( - std::shared_ptr result) { - SetGameConfigValue( - xe::utf8::fix_path_separators(*cpptoml::get_impl(result))); -} -template -CommandVar::CommandVar(const char* name, T* default_value, - const char* description) - : name_(name), - default_value_(*default_value), - description_(description), - current_value_(default_value) {} - -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::EscapeString(val); -} -template <> -inline std::string CommandVar::ToString( - std::filesystem::path val) { - return toml::EscapeString( - xe::utf8::fix_path_separators(xe::path_to_utf8(val), '/')); -} - -template -std::string CommandVar::ToString(T val) { - return std::to_string(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 -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(); -} - -extern std::map* CmdVars; -extern std::map* ConfigVars; - -inline void AddConfigVar(IConfigVar* cv) { - if (!ConfigVars) ConfigVars = new std::map(); - ConfigVars->insert(std::pair(cv->name(), cv)); -} -inline void AddCommandVar(ICommandVar* cv) { - if (!CmdVars) CmdVars = new std::map(); - CmdVars->insert(std::pair(cv->name(), cv)); -} -void ParseLaunchArguments(int& argc, char**& argv, - const std::string_view positional_help, - const std::vector& positional_options); template -T* 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 default_value; +ConfigVar* register_configvar(std::string_view name, T* default_value, + std::string_view description, + std::string_view category, bool is_transient, + bool requires_restart) { + // config instance claims ownership of this pointer + ConfigVar* var = + new ConfigVar{name, default_value, description, + category, is_transient, requires_restart}; + return Config::Instance().RegisterConfigVar(var); } template -T* define_cmdvar(const char* name, T* default_value, const char* description) { - ICommandVar* cmdVar = new CommandVar(name, default_value, description); - AddCommandVar(cmdVar); - return default_value; +CommandVar* register_commandvar(std::string_view name, T* default_value, + std::string_view description) { + // config instance claims ownership of this pointer + CommandVar* var = new CommandVar{name, default_value, description}; + return Config::Instance().RegisterCommandVar(var); } #define DEFINE_bool(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, false, bool) + DEFINE_CVar(name, default_value, description, category, false, false, bool) #define DEFINE_int32(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, false, int32_t) + DEFINE_CVar(name, default_value, description, category, false, false, int32_t) -#define DEFINE_uint64(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, false, uint64_t) +#define DEFINE_uint64(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, false, \ + uint64_t) #define DEFINE_double(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, false, double) + DEFINE_CVar(name, default_value, description, category, false, false, double) -#define DEFINE_string(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, false, std::string) +#define DEFINE_string(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, false, \ + std::string) -#define DEFINE_path(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, false, \ +#define DEFINE_path(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, false, \ + std::filesystem::path) + +#define DEFINE_unrestartable_bool(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, true, bool) + +#define DEFINE_unrestartable_int32(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, true, int32_t) + +#define DEFINE_unrestartable_uint64(name, default_value, description, \ + category) \ + DEFINE_CVar(name, default_value, description, category, false, true, uint64_t) + +#define DEFINE_unrestartable_double(name, default_value, description, \ + category) \ + DEFINE_CVar(name, default_value, description, category, false, true, double) + +#define DEFINE_unrestartable_string(name, default_value, description, \ + category) \ + DEFINE_CVar(name, default_value, description, category, false, true, \ + std::string) + +#define DEFINE_unrestartable_path(name, default_value, description, category) \ + DEFINE_CVar(name, default_value, description, category, false, true, \ 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_CVar(name, default_value, description, category, true, false, \ + std::string) #define DEFINE_transient_path(name, default_value, description, category) \ - DEFINE_CVar(name, default_value, description, category, true, \ + DEFINE_CVar(name, default_value, description, category, true, false, \ std::filesystem::path) #define DEFINE_CVar(name, default_value, description, category, is_transient, \ - type) \ - namespace cvars { \ + requires_restart, type) \ + namespace xe::cvars { \ type name = default_value; \ - } \ - namespace cv { \ - static auto cv_##name = cvar::define_configvar( \ - #name, &cvars::name, description, category, is_transient); \ - } + static auto cv_##name = xe::cvar::register_configvar( \ + #name, &cvars::name, description, category, is_transient, \ + requires_restart); \ + } // namespace cvars // 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 auto cv_##name = \ - cvar::define_cmdvar(#name, &cvars::name, description); \ - } +#define CmdVar(name, default_value, description) \ + namespace xe::cvars { \ + std::string name = default_value; \ + static auto cmdvar_##name = xe::cvar::register_commandvar( \ + #name, &cvars::name, description); \ + } // namespace cvars #define DECLARE_bool(name) DECLARE_CVar(name, bool) @@ -341,10 +236,14 @@ T* define_cmdvar(const char* name, T* default_value, const char* description) { #define DECLARE_path(name) DECLARE_CVar(name, std::filesystem::path) #define DECLARE_CVar(name, type) \ - namespace cvars { \ + namespace xe::cvars { \ extern type name; \ } +// load template implementations +#include "cvar.inc" + } // namespace cvar +} // namespace xe #endif // XENIA_CVAR_H_ diff --git a/src/xenia/base/cvar.inc b/src/xenia/base/cvar.inc new file mode 100644 index 000000000..8140fe8e5 --- /dev/null +++ b/src/xenia/base/cvar.inc @@ -0,0 +1,117 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2020 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +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_].as(); + set_command_line_value(value); +} +template <> +inline void CommandVar::LoadFromLaunchOptions( + cxxopts::ParseResult* result) { + std::string value = (*result)[name_].as(); + set_command_line_value(value); +} +template +void ConfigVar::LoadConfigValue(std::shared_ptr result) { + set_config_value(*cpptoml::get_impl(result)); +} +template <> +inline void ConfigVar::LoadConfigValue( + std::shared_ptr result) { + set_config_value( + xe::utf8::fix_path_separators(*cpptoml::get_impl(result))); +} +template +void ConfigVar::LoadGameConfigValue(std::shared_ptr result) { + set_game_config_value(*cpptoml::get_impl(result)); +} +template <> +inline void ConfigVar::LoadGameConfigValue( + std::shared_ptr result) { + set_game_config_value( + xe::utf8::fix_path_separators(*cpptoml::get_impl(result))); +} +template +CommandVar::CommandVar(std::string_view name, T* default_value, + std::string_view description) + : name_(name), + default_value_(*default_value), + description_(description), + current_value_(default_value) {} + +template +ConfigVar::ConfigVar(std::string_view name, T* default_value, + std::string_view description, std::string_view category, + bool is_transient, bool requires_restart) + : CommandVar(name, default_value, description), + category_(category), + is_transient_(is_transient), + requires_restart_(requires_restart) {} + +template +void CommandVar::UpdateValue() { + if (commandline_value_) return set_current_value(*commandline_value_); + return set_current_value(default_value_); +} +template +void ConfigVar::UpdateValue() { + if (this->command_line_value()) { + return this->set_config_value(*this->command_line_value()); + } + if (game_config_value_) return this->set_current_value(*game_config_value_); + if (config_value_) return this->set_current_value(*config_value_); + return this->set_current_value(this->default_value()); +} + +template <> +inline std::string CommandVar::Convert(std::string_view val) const { + return std::string(val); +} +template <> +inline std::filesystem::path CommandVar::Convert( + std::string_view val) const { + return xe::to_path(val); +} + +template <> +inline std::string CommandVar::ToString(const bool& val) { + return val ? "true" : "false"; +} +template <> +inline std::string CommandVar::ToString(const std::string& val) { + return toml::EscapeString(val); +} +template <> +inline std::string CommandVar::ToString(const + std::filesystem::path& val) { + return toml::EscapeString( + xe::utf8::fix_path_separators(xe::path_to_utf8(val), '/')); +} + diff --git a/src/xenia/base/main_posix.cc b/src/xenia/base/main_posix.cc index 673ff0ac4..39aed151c 100644 --- a/src/xenia/base/main_posix.cc +++ b/src/xenia/base/main_posix.cc @@ -9,6 +9,7 @@ #include "xenia/base/cvar.h" #include "xenia/base/main.h" +#include "xenia/config.h" #include "xenia/base/filesystem.h" #include "xenia/base/logging.h" @@ -23,8 +24,8 @@ bool has_console_attached() { return true; } extern "C" int main(int argc, char** argv) { auto entry_info = xe::GetEntryInfo(); - cvar::ParseLaunchArguments(argc, argv, entry_info.positional_usage, - entry_info.positional_options); + Config::Instance().ParseLaunchArguments( + argc, argv, entry_info.positional_usage, entry_info.positional_options); std::vector args; for (int n = 0; n < argc; n++) { diff --git a/src/xenia/base/main_win.cc b/src/xenia/base/main_win.cc index 44244ec96..040a24bcf 100644 --- a/src/xenia/base/main_win.cc +++ b/src/xenia/base/main_win.cc @@ -26,6 +26,7 @@ // Includes Windows headers, so it goes here. #include "third_party/xbyak/xbyak/xbyak_util.h" +#include "xenia/config.h" DEFINE_bool(win32_high_freq, true, "Requests high performance from the NT kernel", "Kernel"); @@ -106,7 +107,8 @@ static bool parse_launch_arguments(const xe::EntryInfo& entry_info, LocalFree(wargv); - cvar::ParseLaunchArguments(argc, argv, entry_info.positional_usage, + Config::Instance().ParseLaunchArguments( + argc, argv, entry_info.positional_usage, entry_info.positional_options); args.clear(); diff --git a/src/xenia/config.cc b/src/xenia/config.cc index d48fe36e5..34fa14f1e 100644 --- a/src/xenia/config.cc +++ b/src/xenia/config.cc @@ -17,6 +17,8 @@ #include "xenia/base/string.h" #include "xenia/base/string_buffer.h" +CmdVar(config, "", "Specifies the target config to load."); + std::shared_ptr ParseFile( const std::filesystem::path& filename) { std::ifstream file(filename); @@ -28,54 +30,61 @@ std::shared_ptr ParseFile( return p.parse(); } -CmdVar(config, "", "Specifies the target config to load."); -namespace config { -std::string config_name = "xenia.config.toml"; -std::filesystem::path config_folder; -std::filesystem::path config_path; -std::string game_config_suffix = ".config.toml"; +namespace xe { + +Config& Config::Instance() { + static Config config; + return config; +} + +Config::Config() : options_("xenia", "Xbox 30 Emulator") { + +} std::shared_ptr ParseConfig( const std::filesystem::path& config_path) { try { return ParseFile(config_path); - } catch (cpptoml::parse_exception e) { + } catch (const cpptoml::parse_exception& e) { xe::FatalError(fmt::format("Failed to parse config file '{}':\n\n{}", xe::path_to_utf8(config_path), e.what())); return nullptr; } } -void ReadConfig(const std::filesystem::path& file_path) { - const auto config = ParseConfig(file_path); - for (auto& it : *cvar::ConfigVars) { - auto config_var = static_cast(it.second); - auto config_key = config_var->category() + "." + config_var->name(); - if (config->contains_qualified(config_key)) { - config_var->LoadConfigValue(config->get_qualified(config_key)); +void Config::SetupConfig(const std::filesystem::path& config_folder) { + Config::config_folder_ = config_folder; + // check if the user specified a specific config to load + if (!cvars::config.empty()) { + config_path_ = xe::to_path(cvars::config); + if (std::filesystem::exists(config_path_)) { + ReadConfig(config_path_); + return; } } - XELOGI("Loaded config: {}", xe::path_to_utf8(file_path)); -} - -void ReadGameConfig(const std::filesystem::path& file_path) { - const auto config = ParseConfig(file_path); - for (auto& it : *cvar::ConfigVars) { - auto config_var = static_cast(it.second); - auto config_key = config_var->category() + "." + config_var->name(); - if (config->contains_qualified(config_key)) { - config_var->LoadGameConfigValue(config->get_qualified(config_key)); + // if the user specified a --config argument, but the file doesn't exist, + // let's also load the default config + if (!config_folder.empty()) { + config_path_ = config_folder / config_name_; + if (std::filesystem::exists(config_path_)) { + ReadConfig(config_path_); } + // we only want to save the config if the user is using the default + // config, we don't want to override a user created specific config + SaveConfig(); } - XELOGI("Loaded game config: {}", xe::path_to_utf8(file_path)); } -void SaveConfig() { +SaveStatus Config::SaveConfig() { std::vector vars; - for (const auto& s : *cvar::ConfigVars) { - vars.push_back(s.second); + + SaveStatus status = SaveStatus::Saved; + + for (const auto& s : config_vars_) { + vars.push_back(s.second.get()); } - std::sort(vars.begin(), vars.end(), [](auto a, auto b) { + std::sort(vars.begin(), vars.end(), [](auto a, auto b) + { if (a->category() < b->category()) return true; if (a->category() > b->category()) return false; if (a->name() < b->name()) return true; @@ -87,11 +96,15 @@ void SaveConfig() { std::string last_category; bool last_multiline_description = false; xe::StringBuffer sb; - for (auto config_var : vars) { + for (const cvar::IConfigVar* config_var : vars) { if (config_var->is_transient()) { continue; } + if (config_var->requires_restart()) { + status = SaveStatus::RequiresRestart; + } + if (last_category != config_var->category()) { if (!last_category.empty()) { sb.Append('\n', 2); @@ -150,47 +163,120 @@ void SaveConfig() { } // save the config file - xe::filesystem::CreateParentFolder(config_path); + xe::filesystem::CreateParentFolder(config_path_); - auto handle = xe::filesystem::OpenFile(config_path, "wb"); + auto handle = xe::filesystem::OpenFile(config_path_, "wb"); if (!handle) { - XELOGE("Failed to open '{}' for writing.", xe::path_to_utf8(config_path)); - } else { - fwrite(sb.buffer(), 1, sb.length(), handle); - fclose(handle); + XELOGE("Failed to open '{}' for writing.", xe::path_to_utf8(config_path_)); + return SaveStatus::CouldNotSave; } + fwrite(sb.buffer(), 1, sb.length(), handle); + fclose(handle); + + return status; } -void SetupConfig(const std::filesystem::path& config_folder) { - config::config_folder = config_folder; - // check if the user specified a specific config to load - if (!cvars::config.empty()) { - config_path = xe::to_path(cvars::config); - if (std::filesystem::exists(config_path)) { - ReadConfig(config_path); - return; - } - } - // if the user specified a --config argument, but the file doesn't exist, - // let's also load the default config - if (!config_folder.empty()) { - config_path = config_folder / config_name; - if (std::filesystem::exists(config_path)) { - ReadConfig(config_path); - } - // we only want to save the config if the user is using the default - // config, we don't want to override a user created specific config - SaveConfig(); - } -} - -void LoadGameConfig(const std::string_view title_id) { - const auto game_config_folder = config_folder / "config"; +void Config::LoadGameConfig(const std::string_view title_id) { + const auto game_config_folder = Config::config_folder_ / "config"; const auto game_config_path = - game_config_folder / (std::string(title_id) + game_config_suffix); + game_config_folder / (std::string(title_id) + Config::game_config_suffix_); if (std::filesystem::exists(game_config_path)) { ReadGameConfig(game_config_path); } } -} // namespace config +cvar::IConfigVar* Config::FindConfigVar(const std::string& name) { + const auto it = config_vars_.find(name); + if (it != config_vars_.end()) { + return it->second.get(); + } + return nullptr; +} + +cvar::ICommandVar* Config::FindCommandVar(const std::string& name) { + const auto it = command_vars_.find(name); + if (it != command_vars_.end()) { + return it->second.get(); + } + return nullptr; +} + +void Config::ParseLaunchArguments(int& argc, char**& argv, + const std::string_view positional_help, + const std::vector& positional_options) { + options_.add_options()("help", "Prints help and exit."); + + for (auto& it : command_vars_) { + const auto& cmdVar = it.second; + cmdVar->AddToLaunchOptions(&options_); + } + + for (const auto& it : config_vars_) { + const auto& configVar = it.second; + configVar->AddToLaunchOptions(&options_); + } + + try { + options_.positional_help(std::string(positional_help)); + options_.parse_positional(positional_options); + + auto result = options_.parse(argc, argv); + if (result.count("help")) { + PrintHelpAndExit(); + } + + for (auto& it : command_vars_) { + const auto& cmdVar = it.second; + if (result.count(cmdVar->name())) { + cmdVar->LoadFromLaunchOptions(&result); + } + } + + for (auto& it : config_vars_) { + const auto& configVar = it.second; + if (result.count(configVar->name())) { + configVar->LoadFromLaunchOptions(&result); + } + } + } catch (const cxxopts::OptionException& e) { + std::cout << e.what() << std::endl; + PrintHelpAndExit(); + } +} + +void Config::ReadConfig(const std::filesystem::path& file_path) { + const auto config = ParseConfig(file_path); + + for (auto& it : config_vars_) { + const auto& config_var = it.second; + auto config_key = config_var->category() + "." + config_var->name(); + + if (config->contains_qualified(config_key)) { + config_var->LoadConfigValue(config->get_qualified(config_key)); + } + } + XELOGI("Loaded config: {}", xe::path_to_utf8(file_path)); +} + +void Config::ReadGameConfig(const std::filesystem::path& file_path) { + const auto config = ParseConfig(file_path); + + for (auto& it : config_vars_) { + const auto& config_var = it.second; + auto config_key = config_var->category() + "." + config_var->name(); + + if (config->contains_qualified(config_key)) { + config_var->LoadGameConfigValue(config->get_qualified(config_key)); + } + } + XELOGI("Loaded game config: {}", xe::path_to_utf8(file_path)); +} + +void Config::PrintHelpAndExit() { + std::cout << options_.help({""}) << std::endl; + std::cout << "For the full list of command line arguments, see xenia.cfg." + << std::endl; + exit(0); +} + +} // namespace xe diff --git a/src/xenia/config.h b/src/xenia/config.h index 63ef265d5..ea103ed41 100644 --- a/src/xenia/config.h +++ b/src/xenia/config.h @@ -11,10 +11,113 @@ #define XENIA_CONFIG_H_ #include +#include +#include "cxxopts/include/cxxopts.hpp" -namespace config { -void SetupConfig(const std::filesystem::path& config_folder); -void LoadGameConfig(const std::string_view title_id); -} // namespace config +namespace xe { + +namespace cvar { + +class IConfigVar; +class ICommandVar; + +template +class ConfigVar; +template +class CommandVar; + +} // namespace cvar + +enum class SaveStatus : int { + Saved = 1 << 0, + RequiresRestart = 1 << 0 | 1 << 1, + CouldNotSave = 1 << 2 +}; + +using IConfigVarRef = std::unique_ptr; +using ICommandVarRef = std::unique_ptr; + +class Config { + public: + static Config& Instance(); + /** + * Populates the global config variables from a config file + */ + void SetupConfig(const std::filesystem::path& config_folder); + + /** + * Save the current config variable state to the config file + */ + SaveStatus SaveConfig(); + + /** + * Overrides config variables with those from a game-specific config file (if + * any) + */ + void LoadGameConfig(std::string_view title_id); + + /** + * Find a config variable for a given config field + */ + cvar::IConfigVar* FindConfigVar(const std::string& name); + + /** + * Find a command variable for a given name + */ + cvar::ICommandVar* FindCommandVar(const std::string& name); + + /** + * Parse launch arguments and update cvar values based on provided launch + * parameters + */ + void ParseLaunchArguments(int& argc, char**& argv, + std::string_view positional_help, + const std::vector& positional_options); + + /** + * Register a config var to the config system + * Claims ownership of the provided pointer + */ + template + cvar::ConfigVar* RegisterConfigVar(cvar::ConfigVar* var); + + /** + * Register a command var to the config system + * Claims ownership of the provided pointer + */ + template + cvar::CommandVar* RegisterCommandVar(cvar::CommandVar* var); + + private: + Config(); + + void ReadConfig(const std::filesystem::path& file_path); + void ReadGameConfig(const std::filesystem::path& file_path); + void PrintHelpAndExit(); + + std::unordered_map config_vars_; + std::unordered_map command_vars_; + std::filesystem::path config_folder_; + std::filesystem::path config_path_; + std::string config_name_ = "xenia.config.toml"; + std::string game_config_suffix_ = ".config.toml"; + cxxopts::Options options_; +}; + +template +cvar::ConfigVar* Config::RegisterConfigVar(cvar::ConfigVar* var) { + const std::string& name = var->name(); + config_vars_[name] = std::unique_ptr>(var); + return dynamic_cast*>(config_vars_[name].get()); +} + +template +cvar::CommandVar* Config::RegisterCommandVar(cvar::CommandVar* var) { + const std::string& name = var->name(); + command_vars_[name] = std::unique_ptr>(var); + return dynamic_cast*>(command_vars_[name].get()); +} + +} // namespace xe #endif // XENIA_CONFIG_H_ diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 5b2d17527..e584dac80 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -637,7 +637,7 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, // Try and load the resource database (xex only). if (module->title_id()) { auto title_id = fmt::format("{:08X}", module->title_id()); - config::LoadGameConfig(title_id); + Config::Instance().LoadGameConfig(title_id); uint32_t resource_data = 0; uint32_t resource_size = 0; if (XSUCCEEDED(module->GetSection(title_id.c_str(), &resource_data, diff --git a/tools/build/src/test_suite_main.cc b/tools/build/src/test_suite_main.cc index b5daf3972..d443b8958 100644 --- a/tools/build/src/test_suite_main.cc +++ b/tools/build/src/test_suite_main.cc @@ -14,8 +14,10 @@ #include #define CATCH_CONFIG_RUNNER + #include "third_party/catch/include/catch.hpp" #include "xenia/base/cvar.h" +#include "xenia/config.h" namespace xe { @@ -23,7 +25,8 @@ bool has_console_attached() { return true; } // Used in console mode apps; automatically picked based on subsystem. int Main(int argc, char* argv[]) { - cvar::ParseLaunchArguments(argc, argv, "", std::vector()); + Config::Instance().ParseLaunchArguments(argc, argv, "", + std::vector()); // Run Catch. int result = Catch::Session().run(argc, argv);