GUI2: harden theme definition parsing (#9974)

- Ensure current_gui is a valid iterator after calling init(). It should match default_gui
- Ensure current_gui is always set to guis.end() if the requested theme isn't found in switch_theme().
  It's initialized as such in gui_definition.cpp, but we shouldn't rely on that always being the case,
  especially in a function that changes it.
- Only create one schema validator for all themes. Previously, it would have been created anew for
   each addon with a custom theme.
- Add explicit exception handling for an invalid schema path or invalid gui2 entry point
- Add explicit warning for addon themes with the id "default"
- Catch `wml_exception` when creating gui_definition objects for all themes (previously, this was only
   caught for add-on themes)
- Adjust error messages
- Fix duplicate theme errors when reloading cache (addresses one part of #9781). GUI2 definition
  changes now take effect without the need to restart the entire game. A cache reload is sufficient.
- Use `_main.cfg` for core theme entry points
- Include macros at global core GUI2 score instead of only in the default theme (they were always
  available to both themes, this just makes that clearer)
This commit is contained in:
Charles Dang 2025-03-02 00:12:32 -05:00 committed by GitHub
parent e8c164d377
commit 55dc0a8215
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 132 additions and 98 deletions

View File

@ -1,8 +1,11 @@
# This is the main config file for the GUI definitions
# Include the files containing [gui] tags here.
# Helper macros common to all themes
{./macros/}
# Default Theme, Wesnoth Classic
{./themes/default/default.cfg}
{./themes/default}
# Wesnoth Blue Sky Theme
{./themes/celes/celes.cfg}
{./themes/celes}

View File

@ -25,7 +25,6 @@
{tips.cfg}
{gui/macros/}
{gui/themes/default/widgets/}
{gui/themes/default/dialogs/}

View File

@ -30,6 +30,97 @@
namespace gui2
{
namespace
{
class theme_parser
{
public:
theme_parser()
try
: validator_(filesystem::get_wml_location("schema/gui.cfg").value())
, defines_(game_config::config_cache::instance().get_preproc_map())
{
guis.clear(); // Reset in case we're re-initializing the UI subsystem
} catch(const utils::bad_optional_access&) {
FAIL("GUI2: schema/gui.cfg not found.");
} catch(const abstract_validator::error& e) {
FAIL("GUI2: could not read schema file: " + e.message);
}
/**
* Parses any GUI2 theme definitions at the file path specified.
*
* @param full_path Path to file containing one or more [gui] tags.
* @param is_core If true, look for the default theme here.
*/
void parse(const std::string& full_path, bool is_core)
{
#if __cpp_range_based_for >= 202211L // lifetime extension of temporaries
for(const config& def : read_and_validate(full_path).child_range("gui")) {
#else
config cfg = read_and_validate(full_path);
for(const config& def : cfg.child_range("gui")) {
#endif
const bool is_default = def["id"] == "default";
if(is_default && !is_core) {
ERR_GUI_P << "UI theme id 'default' is reserved for core themes.";
continue;
}
const auto iter = register_theme(def);
if(!iter) continue;
if(is_default && is_core) {
default_gui = *iter;
current_gui = default_gui;
}
}
}
private:
auto read_and_validate(const std::string& path) -> config
try {
filesystem::scoped_istream stream = preprocess_file(path, &defines_);
return ::read(*stream, &validator_);
} catch(const config::error& e) {
ERR_GUI_P << "Could not read gui file: " << path;
ERR_GUI_P << e.what();
return {};
};
/**
* Adds a theme definition object to the global registry.
*
* @param def A valid gui_definition config.
*
* @returns An optional iterator to the newly-constructed definition.
* If errors occurred while parsing the config or a theme with
* the given ID already exists, returns nullopt.
*/
auto register_theme(const config& def) const -> utils::optional<gui_theme_map_t::iterator>
try {
auto [iter, is_unique] = guis.try_emplace(def["id"], def);
if(is_unique) return iter;
ERR_GUI_P << "UI Theme '" << def["id"] << "' already exists.";
return utils::nullopt;
} catch(const wml_exception& e) {
ERR_GUI_P << "Invalid UI theme: " << def["id"];
ERR_GUI_P << e.user_message;
return utils::nullopt;
}
/** GUI2 schema validator. */
schema_validation::schema_validator validator_;
/** Common macro context shared by all themes (@todo: document this fact). */
preproc_map defines_;
};
} // namespace
void init()
{
@ -38,118 +129,49 @@ void init()
// Save current screen size.
settings::update_screen_size_variables();
config guis_cfg, addons_cfg;
preproc_map preproc(game_config::config_cache::instance().get_preproc_map());
//
// Read and validate theme WML files from mainline
//
std::string current_file;
const std::string schema_file = "schema/gui.cfg";
try {
schema_validation::schema_validator validator(filesystem::get_wml_location(schema_file).value());
// Core theme files
current_file = "gui/_main.cfg";
filesystem::scoped_istream stream = preprocess_file(filesystem::get_wml_location(current_file).value(), &preproc);
read(guis_cfg, *stream, &validator);
} catch(const config::error& e) {
ERR_GUI_P << e.what();
ERR_GUI_P << "Setting: could not read gui file: " << current_file;
} catch(const abstract_validator::error& e) {
ERR_GUI_P << "Setting: could not read schema file: " << schema_file;
ERR_GUI_P << e.message;
}
//
// Read and validate theme WML files from addons
//
// Add the $user_campaign_dir/*/gui.cfg files to the addon gui config.
std::vector<std::string> user_dirs;
{
const std::string user_campaign_dir = filesystem::get_addons_dir();
std::vector<std::string> user_files;
filesystem::get_files_in_dir(
user_campaign_dir, &user_files, &user_dirs, filesystem::name_mode::ENTIRE_FILE_PATH);
}
for(const std::string& umc : user_dirs) {
try {
const std::string gui_file = umc + "/gui-theme.cfg";
current_file = filesystem::get_short_wml_path(gui_file);
if(filesystem::file_exists(gui_file)) {
config addon_cfg;
schema_validation::schema_validator validator(filesystem::get_wml_location(schema_file).value());
read(addon_cfg, *preprocess_file(gui_file, &preproc), &validator);
addons_cfg.append(addon_cfg);
}
} catch(const config::error& e) {
ERR_GUI_P << e.what();
ERR_GUI_P << "Setting: could not read gui file: " << current_file;
} catch(const abstract_validator::error& e) {
ERR_GUI_P << "Setting: could not read schema file: " << schema_file;
ERR_GUI_P << e.message;
}
}
//
// Parse GUI definitions from mainline
//
for(const config& g : guis_cfg.child_range("gui")) {
const std::string id = g["id"];
auto [iter, is_unique] = guis.try_emplace(id, g);
theme_parser parser;
if (!is_unique) {
ERR_GUI_P << "GUI Theme ID '" << id << "' already exists.";
} else {
if(id == "default") {
default_gui = iter;
}
}
try {
parser.parse(filesystem::get_wml_location("gui/_main.cfg").value(), true);
} catch(const utils::bad_optional_access&) {
FAIL("GUI2: gui/_main.cfg not found.");
}
// The default GUI must be in mainline
VALIDATE(default_gui != guis.end(), _("No default gui defined."));
//
// Parse GUI definitions from addons
//
for(const config& g : addons_cfg.child_range("gui")) {
const std::string id = g["id"];
try {
auto [iter, is_unique] = guis.try_emplace(id, g);
std::vector<std::string> addon_dirs;
const std::string umc_dir = filesystem::get_addons_dir();
filesystem::get_files_in_dir(umc_dir, nullptr, &addon_dirs, filesystem::name_mode::ENTIRE_FILE_PATH);
if (!is_unique) {
ERR_GUI_P << "GUI Theme ID '" << id << "' already exists.";
}
} catch (const wml_exception& e) {
ERR_GUI_P << "Non-functional theme: " << id;
ERR_GUI_P << e.user_message;
// Search for all $user_campaign_dir/*/gui-theme.cfg files
for(const std::string& umc : addon_dirs) {
const std::string gui_file = umc + "/gui-theme.cfg";
if(filesystem::file_exists(gui_file)) {
parser.parse(gui_file, false);
}
}
VALIDATE(default_gui != guis.end(), _("No default gui defined."));
}
void switch_theme(const std::string& current_theme)
void switch_theme(const std::string& theme_id)
{
if (current_theme.empty() || current_theme == "default") {
if(theme_id.empty() || theme_id == "default") {
current_gui = default_gui;
} else {
gui_theme_map_t::iterator gui_itor = guis.begin();
for (const auto& gui : guis) {
if (gui.first == current_theme) {
current_gui = gui_itor;
}
if (gui_itor != guis.end()) {
gui_itor++;
}
}
current_gui = std::find_if(guis.begin(), guis.end(),
[&](const auto& theme) { return theme.first == theme_id; });
if(current_gui == guis.end()) {
ERR_GUI_P << "Missing [gui] definition for '" << current_theme << "'";
ERR_GUI_P << "Missing [gui] definition for '" << theme_id << "'";
current_gui = default_gui;
}
}

View File

@ -23,16 +23,17 @@ namespace gui2
* Initializes the GUI subsystems.
*
* @note This function must be called before other parts of the UI engine
* are used. Use @ref switch_theme below to actually activate the theme.
* are used. Use @ref switch_theme below to actually activate a theme.
*
* @post The default_gui and current_gui iterators are valid and equal.
*/
void init();
/**
* Set and activate the given gui2 theme
*
* @param current_theme the name of the gui2 theme to switch to
* @param theme_id The id of the gui2 theme to switch to
*/
void switch_theme(const std::string& current_theme);
void switch_theme(const std::string& theme_id);
} // namespace gui2

View File

@ -637,6 +637,13 @@ void read(config& cfg, const std::string& in, abstract_validator* validator)
parser(cfg, ss, validator)();
}
config read(std::istream& in, abstract_validator* validator)
{
config cfg;
parser(cfg, in, validator)();
return cfg;
}
template<typename decompressor>
void read_compressed(config& cfg, std::istream& file, abstract_validator* validator)
{

View File

@ -29,6 +29,8 @@ void read(config& cfg, const std::string& in, abstract_validator* validator = nu
void read_gz(config& cfg, std::istream& in, abstract_validator* validator = nullptr);
void read_bz2(config& cfg, std::istream& in, abstract_validator* validator = nullptr);
config read(std::istream& in, abstract_validator* validator = nullptr); // Throws config::error
void write(std::ostream& out, const configr_of& cfg, unsigned int level = 0);
void write_gz(std::ostream& out, const configr_of& cfg);
void write_bz2(std::ostream& out, const configr_of& cfg);