diff --git a/data/schema/game_config.cfg b/data/schema/game_config.cfg index a4cde4224988..7a89805d076b 100644 --- a/data/schema/game_config.cfg +++ b/data/schema/game_config.cfg @@ -1,5 +1,3 @@ -#textdomain wesnoth - {./macros.cfg} [wml_schema] @@ -161,6 +159,21 @@ name="server_address" value="[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)+:\d+" [/type] + [type] + name="effect_times" + [union] + [type] + link=int + [/type] + [type] + value="per level" + [/type] + [/union] + [/type] + [type] + name="effect_set_special_mode" + value="append|replace" + [/type] [tag] name="root" min=1 diff --git a/data/schema/units/animations.cfg b/data/schema/units/animations.cfg index bb8a1f975ab7..b5eda147fa2a 100644 --- a/data/schema/units/animations.cfg +++ b/data/schema/units/animations.cfg @@ -79,6 +79,7 @@ [/tag] # Specific animation tags +# Any new tag added here must also be linked from [effect] [tag] name="animation" max=infinite @@ -121,11 +122,6 @@ max=infinite super="units/unit_type/$animation" [/tag] -[tag] - name="defend" - max=infinite - super="units/unit_type/$animation" -[/tag] [tag] name="pre_movement_anim" max=infinite diff --git a/data/schema/units/modifications.cfg b/data/schema/units/modifications.cfg index 17c442943f93..ba27d0903f5e 100644 --- a/data/schema/units/modifications.cfg +++ b/data/schema/units/modifications.cfg @@ -7,8 +7,214 @@ max=infinite # TODO: The schema of this tag depends on the value of apply_to... {REQUIRED_KEY apply_to string} - {ANY_KEY string} - any_tag=yes + {DEFAULT_KEY times effect_times 1} + {FILTER_TAG "filter" unit ()} + [switch] + key=apply_to + [case] + value=new_attack + super="units/unit_type/attack" + [/case] + {FILTER_TAG "case" weapon value=remove_attacks} + {FILTER_TAG "case" weapon ( + value=attack + {SIMPLE_KEY set_name string} + {SIMPLE_KEY set_description t_string} + {SIMPLE_KEY set_type string} + {SIMPLE_KEY set_icon string} + {SIMPLE_KEY set_range string} + + {SIMPLE_KEY set_damage int} + {SIMPLE_KEY set_attacks int} + {SIMPLE_KEY set_parry int} + {SIMPLE_KEY set_accuracy int} + {SIMPLE_KEY set_movement_used int} + + {SIMPLE_KEY increase_damage int} + {SIMPLE_KEY increase_attacks int} + {SIMPLE_KEY increase_parry int} + {SIMPLE_KEY increase_accuracy int} + {SIMPLE_KEY increase_movement_used int} + + {SIMPLE_KEY attack_weight int} + {SIMPLE_KEY defense_weight int} + {SIMPLE_KEY remove_specials string_list} + + [tag] + name="set_specials" + super="units/unit_type/attack/specials" + {DEFAULT_KEY mode effect_set_special_mode replace} + [/tag] + )} + [case] + value=max_attacks,max_experience + {SIMPLE_KEY increase int_percent} + [/case] + [case] + value=movement,vision,jamming,experience,recall_cost + {SIMPLE_KEY increase int_percent} + {SIMPLE_KEY set int} + [/case] + [case] + value=hitpoints + {SIMPLE_KEY increase int_percent} + {DEFAULT_KEY heal_full bool yes} + {SIMPLE_KEY increase_total int_percent} + {DEFAULT_KEY violate_maximum bool no} + [/case] + [case] + value=loyal + # Nothing allowed here + [/case] + [case] + value=movement_costs + {SIMPLE_KEY replace bool} + [link] + name="units/movetype/movement_costs" + [/link] + [/case] + [case] + value=vision_costs + {SIMPLE_KEY replace bool} + [link] + name="units/movetype/vision_costs" + [/link] + [/case] + [case] + value=jamming_costs + {SIMPLE_KEY replace bool} + [link] + name="units/movetype/jamming_costs" + [/link] + [/case] + [case] + value=defense + {SIMPLE_KEY replace bool} + [link] + name="units/movetype/defense" + [/link] + [/case] + [case] + value=resistance + {SIMPLE_KEY replace bool} + [link] + name="units/movetype/resistance" + [/link] + [/case] + [case] + value=variation,type + {SIMPLE_KEY name string} + [/case] + [case] + value=status + {SIMPLE_KEY add string_list} + {SIMPLE_KEY remove string_list} + [/case] + [case] + value=zoc + {SIMPLE_KEY value bool} + [/case] + [case] + value=profile + {SIMPLE_KEY portrait string} + {SIMPLE_KEY small_portrait string} + {SIMPLE_KEY description t_string} + [/case] + [case] + value=new_ability,remove_ability + [link] + name="units/unit_type/abilities" + [/link] + [/case] + [case] + value=new_animation + [link] + name="units/unit_type/animation" + [/link] + [link] + name="units/unit_type/defend" + [/link] + [link] + name="units/unit_type/death" + [/link] + [link] + name="units/unit_type/standing_anim" + [/link] + [link] + name="units/unit_type/movement_anim" + [/link] + [link] + name="units/unit_type/idle_anim" + [/link] + [link] + name="units/unit_type/attack_anim" + [/link] + [link] + name="units/unit_type/victory_anim" + [/link] + [link] + name="units/unit_type/pre_movement_anim" + [/link] + [link] + name="units/unit_type/post_movement_anim" + [/link] + [link] + name="units/unit_type/draw_weapon_anim" + [/link] + [link] + name="units/unit_type/sheath_weapon_anim" + [/link] + [link] + name="units/unit_type/leading_anim" + [/link] + [link] + name="units/unit_type/recruit_anim" + [/link] + [link] + name="units/unit_type/recruiting_anim" + [/link] + [link] + name="units/unit_type/healing_anim" + [/link] + [link] + name="units/unit_type/extra_anim" + [/link] + [/case] + [case] + value=image_mod,overlay + {SIMPLE_KEY replace string} + {SIMPLE_KEY add string} + [/case] + [case] + value=ellipse + {SIMPLE_KEY ellipse string} + [/case] + [case] + value=halo + {SIMPLE_KEY halo string} + [/case] + [case] + value=alignment + {SIMPLE_KEY set alignment} + [/case] + [case] + value=new_advancement + {SIMPLE_KEY replace bool} + {SIMPLE_KEY types string_list} + [link] + name="units/$modifications/advancement" + [/link] + [/case] + [case] + value=remove_advancement + {SIMPLE_KEY types string_list} + {SIMPLE_KEY amlas string_list} + [/case] + [default] + any_tag=yes + {ANY_KEY string} + [/default] + [/switch] {WML_MERGE_KEYS} [/tag] {WML_MERGE_KEYS} diff --git a/src/serialization/parser.cpp b/src/serialization/parser.cpp index 6ab69e02e128..6fcb6c2754e3 100644 --- a/src/serialization/parser.cpp +++ b/src/serialization/parser.cpp @@ -184,6 +184,7 @@ void parser::parse_element() std::string elname; config* current_element = nullptr; + config* parent = nullptr; switch(tok_.current_token().type) { case token::STRING: // [element] @@ -194,11 +195,12 @@ void parser::parse_element() } // Add the element - current_element = &(elements.top().cfg->add_child(elname)); + parent = elements.top().cfg; + current_element = &(parent->add_child(elname)); elements.emplace(current_element, elname, tok_.get_start_line(), tok_.get_file()); if(validator_) { - validator_->open_tag(elname, tok_.get_start_line(), tok_.get_file()); + validator_->open_tag(elname, *parent, tok_.get_start_line(), tok_.get_file()); } break; @@ -215,17 +217,18 @@ void parser::parse_element() } // Find the last child of the current element whose name is element - if(config& c = elements.top().cfg->child(elname, -1)) { + parent = elements.top().cfg; + if(config& c = parent->child(elname, -1)) { current_element = &c; if(validator_) { - validator_->open_tag(elname, tok_.get_start_line(), tok_.get_file(), true); + validator_->open_tag(elname, *parent, tok_.get_start_line(), tok_.get_file(), true); } } else { - current_element = &elements.top().cfg->add_child(elname); + current_element = &parent->add_child(elname); if(validator_) { - validator_->open_tag(elname, tok_.get_start_line(), tok_.get_file()); + validator_->open_tag(elname, *parent, tok_.get_start_line(), tok_.get_file()); } } diff --git a/src/serialization/schema_validator.cpp b/src/serialization/schema_validator.cpp index f43ab28debc3..9b7c30865788 100644 --- a/src/serialization/schema_validator.cpp +++ b/src/serialization/schema_validator.cpp @@ -189,13 +189,13 @@ bool schema_validator::read_config_file(const std::string& filename) * assume they all are on their place due to parser algorithm * and validation logic */ -void schema_validator::open_tag(const std::string& name, int start_line, const std::string& file, bool addittion) +void schema_validator::open_tag(const std::string& name, const config& parent, int start_line, const std::string& file, bool addittion) { if(!stack_.empty()) { const class_tag* tag = nullptr; if(stack_.top()) { - tag = stack_.top()->find_tag(name, root_); + tag = stack_.top()->find_tag(name, root_, parent); if(!tag) { wrong_tag_error(file, start_line, name, stack_.top()->get_name(), create_exceptions_); @@ -243,7 +243,7 @@ void schema_validator::validate(const config& cfg, const std::string& name, int // Please note that validating unknown tag keys the result will be false // Checking all elements counters. if(!stack_.empty() && stack_.top() && config_read_) { - for(const auto& tag : stack_.top()->tags()) { + for(const auto& tag : stack_.top()->tags(cfg)) { int cnt = counter_.top()[tag.first].cnt; if(tag.second.get_min() > cnt) { @@ -259,7 +259,7 @@ void schema_validator::validate(const config& cfg, const std::string& name, int } // Checking if all mandatory keys are present - for(const auto& key : stack_.top()->keys()) { + for(const auto& key : stack_.top()->keys(cfg)) { if(key.second.is_mandatory()) { if(cfg.get(key.first) == nullptr) { cache_.top()[&cfg].emplace_back(MISSING_KEY, file, start_line, 0, name, key.first); @@ -274,7 +274,7 @@ void schema_validator::validate_key( { if(!stack_.empty() && stack_.top() && !stack_.top()->get_name().empty() && config_read_) { // checking existing keys - const class_key* key = stack_.top()->find_key(name); + const class_key* key = stack_.top()->find_key(name, cfg); if(key) { bool matched = false; for(auto& possible_type : utils::split(key->get_type())) { diff --git a/src/serialization/schema_validator.hpp b/src/serialization/schema_validator.hpp index 322c7c5cc468..4594d5d12d05 100644 --- a/src/serialization/schema_validator.hpp +++ b/src/serialization/schema_validator.hpp @@ -52,7 +52,7 @@ class schema_validator : public abstract_validator } virtual void open_tag( - const std::string& name, int start_line = 0, const std::string& file = "", bool addittion = false); + const std::string& name, const config& parent, int start_line = 0, const std::string& file = "", bool addittion = false); virtual void close_tag(); virtual void validate(const config& cfg, const std::string& name, int start_line, const std::string& file); virtual void validate_key(const config& cfg, diff --git a/src/serialization/tag.cpp b/src/serialization/tag.cpp index 16cb3e90e5ff..624802c7974f 100644 --- a/src/serialization/tag.cpp +++ b/src/serialization/tag.cpp @@ -20,6 +20,7 @@ #include "serialization/tag.hpp" #include "serialization/string_utils.hpp" #include "boost/optional.hpp" +#include "formatter.hpp" #include "config.hpp" @@ -153,6 +154,8 @@ void class_key::print(std::ostream& os, int level) const os << s << " default=" << default_ << "\n"; } + // TODO: Other attributes + os << s << "[/key]\n"; } @@ -189,6 +192,14 @@ class_tag::class_tag(const config& cfg) std::string link_name = link["name"].str(); add_link(link_name); } + + for(const config& sw : cfg.child_range("switch")) { + add_switch(sw); + } + + for(const config& filter : cfg.child_range("if")) { + add_filter(filter); + } } void class_tag::print(std::ostream& os) @@ -204,12 +215,23 @@ void class_tag::add_link(const std::string& link) links_.emplace(name_link, link); } -const class_key* class_tag::find_key(const std::string& name) const +const class_key* class_tag::find_key(const std::string& name, const config& match) const { + // Check the conditions first, so that conditional definitions + // override base definitions in the event of duplicates. + for(auto& cond : conditions_) { + if(cond.matches(match)) { + if(auto key = cond.find_key(name, match)) { + return key; + } + } + } + const auto it_keys = keys_.find(name); if(it_keys != keys_.end()) { return &(it_keys->second); } + key_map::const_iterator it_fuzzy = std::find_if(keys_.begin(), keys_.end(), [&name](const key_map::value_type& key){ if(!key.second.is_fuzzy()) { return false; @@ -232,7 +254,7 @@ const std::string* class_tag::find_link(const std::string& name) const return nullptr; } -const class_tag* class_tag::find_tag(const std::string& fullpath, const class_tag& root) const +const class_tag* class_tag::find_tag(const std::string& fullpath, const class_tag& root, const config& match) const { if(fullpath.empty()) { return nullptr; @@ -249,18 +271,28 @@ const class_tag* class_tag::find_tag(const std::string& fullpath, const class_ta name = fullpath; } + // Check the conditions first, so that conditional definitions + // override base definitions in the event of duplicates. + for(auto& cond : conditions_) { + if(cond.matches(match)) { + if(auto tag = cond.find_tag(fullpath, root, match)) { + return tag; + } + } + } + const auto it_tags = tags_.find(name); if(it_tags != tags_.end()) { if(next_path.empty()) { return &(it_tags->second); } else { - return it_tags->second.find_tag(next_path, root); + return it_tags->second.find_tag(next_path, root, match); } } const auto it_links = links_.find(name); if(it_links != links_.end()) { - return root.find_tag(it_links->second + "/" + next_path, root); + return root.find_tag(it_links->second + "/" + next_path, root, match); } const auto it_fuzzy = std::find_if(tags_.begin(), tags_.end(), [&name](const tag_map::value_type& tag){ @@ -273,7 +305,7 @@ const class_tag* class_tag::find_tag(const std::string& fullpath, const class_ta if(next_path.empty()) { return &(it_fuzzy->second); } else { - return it_tags->second.find_tag(next_path, root); + return it_tags->second.find_tag(next_path, root, match); } } @@ -290,6 +322,10 @@ void class_tag::expand_all(class_tag& root) tag.second.expand(root); tag.second.expand_all(root); } + for(auto& cond : conditions_) { + cond.expand(root); + cond.expand_all(root); + } } void class_tag::remove_keys_by_type(const std::string& type) @@ -366,6 +402,8 @@ void class_tag::printl(std::ostream& os, int level, int step) key.second.print(os, level + step); } + // TODO: Other attributes + os << s << "[/tag]\n"; } @@ -382,6 +420,7 @@ void class_tag::add_tag(const std::string& path, const class_tag& tag, class_tag it->second.add_tags(tag.tags_); it->second.add_keys(tag.keys_); it->second.add_links(tag.links_); + // TODO: Other attributes } links_.erase(tag.get_name()); @@ -411,8 +450,10 @@ void class_tag::add_tag(const std::string& path, const class_tag& tag, class_tag void class_tag::append_super(const class_tag& tag, const std::string& path) { + // TODO: Ensure derived tag definitions override base tag definitions in the event of duplicates add_keys(tag.keys_); add_links(tag.links_); + add_conditions(tag.conditions_); for(const auto& t : tag.tags_) { links_.erase(t.first); @@ -432,13 +473,14 @@ void class_tag::append_super(const class_tag& tag, const std::string& path) void class_tag::expand(class_tag& root) { if(!super_.empty()) { - class_tag* super_tag = root.find_tag(super_, root); + class_tag* super_tag = root.find_tag(super_, root, config()); if(super_tag) { if(super_tag != this) { super_tag->expand(root); append_super(*super_tag, super_); super_.clear(); } else { + // TODO: Detect super cycles too! std::cerr << "the same" << super_tag->name_ << "\n"; } } @@ -446,4 +488,98 @@ void class_tag::expand(class_tag& root) } } +void class_tag::add_switch(const config& switch_cfg) +{ + config default_cfg; + const std::string key = switch_cfg["key"]; + for(const auto& case_cfg : switch_cfg.child_range("case")) { + const std::vector values = utils::split(case_cfg["value"]); + config filter; + for(const auto& value : values) { + filter.add_child("or")[key] = value; + default_cfg.add_child("not")[key] = value; + } + conditions_.emplace_back(case_cfg, filter); + const std::string name = formatter() << get_name() << '[' << key << '=' << case_cfg["value"] << ']'; + conditions_.back().set_name(name); + } + if(switch_cfg.has_child("default")) { + conditions_.emplace_back(switch_cfg.child("default"), default_cfg); + const std::string name = formatter() << get_name() << "[default]"; + conditions_.back().set_name(name); + } +} + +void class_tag::add_filter(const config& cond_cfg) +{ + config filter = cond_cfg; + filter.clear_children("then", "else"); + if(cond_cfg.has_child("then")) { + conditions_.emplace_back(cond_cfg.child("then"), filter); + const std::string name = formatter() << get_name() << "[then]"; + conditions_.back().set_name(name); + } + if(cond_cfg.has_child("else")) { + conditions_.emplace_back(cond_cfg.child("else"), config{"not", filter}); + const std::string name = formatter() << get_name() << "[else]"; + conditions_.back().set_name(name); + } +} + +bool class_condition::matches(const config& cfg) const +{ + if(cfg.empty()) { + // Conditions are not allowed to match an empty config. + // If they were, the conditions might be considered when expanding super-tags. + // That would result in a condition tag being used for the expansion, rather than + // the base tag, which would be bad. + return false; + } + return cfg.matches(filter_); +} + +void class_tag::tag_iterator::init(const class_tag& base_tag) +{ + current = base_tag.tags_.begin(); + if(current != base_tag.tags_.end()) { + condition_queue.push(&base_tag); + } +} + +void class_tag::tag_iterator::increment() +{ + ++current; + while(current== condition_queue.front()->tags_.end()) { + condition_queue.pop(); + if(condition_queue.empty()) { + return; + } + const class_tag& new_base = *condition_queue.front(); + current= new_base.tags_.begin(); + push_new_tag_conditions(new_base); + } +} + +void class_tag::key_iterator::init(const class_tag& base_tag) +{ + current = base_tag.keys_.begin(); + if(current != base_tag.keys_.end()) { + condition_queue.push(&base_tag); + } +} + +void class_tag::key_iterator::increment() +{ + ++current; + while(current == condition_queue.front()->keys_.end()) { + condition_queue.pop(); + if(condition_queue.empty()) { + return; + } + const class_tag& new_base = *condition_queue.front(); + current = new_base.keys_.begin(); + push_new_tag_conditions(new_base); + } +} + } // namespace schema_validation diff --git a/src/serialization/tag.hpp b/src/serialization/tag.hpp index 5373f1a89d1b..d7a05ef1809b 100644 --- a/src/serialization/tag.hpp +++ b/src/serialization/tag.hpp @@ -26,10 +26,12 @@ #include #include #include +#include #include - -class config; +#include +#include +#include "config.hpp" namespace schema_validation { @@ -237,6 +239,8 @@ class class_key bool fuzzy_; }; +class class_condition; + /** * Stores information about tag. * Each tags is an element of great tag tree. This tree is close to filesystem: @@ -251,6 +255,62 @@ class class_tag using tag_map = std::map; using key_map = std::map; using link_map = std::map; + using condition_list = std::vector; +private: + void push_new_tag_conditions(std::queue q, const class_tag& tag); + template> + class iterator : public boost::iterator_facade, const typename Map::value_type, std::forward_iterator_tag> + { + std::queue condition_queue; + typename Map::const_iterator current; + const config& match; + public: + // Construct a begin iterator + iterator(const class_tag& base_tag, const config& match) : match(match) + { + init(base_tag); + push_new_tag_conditions(base_tag); + } + // Construct an end iterator + // That weird expression is to get a reference to an "invalid" config. + iterator() : match(config().child("a")) {} + private: + friend class boost::iterator_core_access; + void init(const class_tag& base_tag); + void increment(); + void push_new_tag_conditions(const class_tag& tag) + { + for(const auto& condition : tag.conditions_) { + if(condition.matches(match)) { + condition_queue.push(&condition); + } + } + } + bool equal(const iterator& other) const + { + if(condition_queue.empty() && other.condition_queue.empty()) { + return true; + } + if(condition_queue.empty() || other.condition_queue.empty()) { + return false; + } + if(condition_queue.front() != other.condition_queue.front()) { + return false; + } + if(current != other.current) { + return false; + } + return true; + } + typename iterator::reference dereference() const + { + return *current; + } + }; + template friend class iterator; + using tag_iterator = iterator; + using key_iterator = iterator; +public: class_tag() : name_("") @@ -386,6 +446,10 @@ class class_tag void add_link(const std::string& link); + void add_switch(const config& switch_cfg); + + void add_filter(const config& cond_cfg); + /** * Tags are usually organized in tree. * This function helps to add a tag to his exact place in tree @@ -408,7 +472,7 @@ class class_tag } /** Returns pointer to child key. */ - const class_key* find_key(const std::string& name) const; + const class_key* find_key(const std::string& name, const config& match) const; /** Returns pointer to child link. */ const std::string* find_link(const std::string& name) const; @@ -417,19 +481,19 @@ class class_tag * Returns pointer to tag using full path to it. * Also work with links */ - const class_tag* find_tag(const std::string& fullpath, const class_tag& root) const; + const class_tag* find_tag(const std::string& fullpath, const class_tag& root, const config& match) const; /** Calls the expansion on each child. */ void expand_all(class_tag& root); - const tag_map& tags() const + boost::iterator_range tags(const config& cfg_match) const { - return tags_; + return {tag_iterator(*this, cfg_match), tag_iterator()}; } - const key_map& keys() const + boost::iterator_range keys(const config& cfg_match) const { - return keys_; + return {key_iterator(*this, cfg_match), key_iterator()}; } const link_map& links() const @@ -437,6 +501,11 @@ class class_tag return links_; } + const condition_list& conditions() const + { + return conditions_; + } + void remove_key_by_name(const std::string& name) { keys_.erase(name); @@ -473,6 +542,9 @@ class class_tag /** links to possible children. */ link_map links_; + /** conditional partial matches */ + condition_list conditions_; + /** whether this is a "fuzzy" tag. */ bool fuzzy_; @@ -488,9 +560,9 @@ class class_tag */ void printl(std::ostream& os, int level, int step = 4); - class_tag* find_tag(const std::string & fullpath, class_tag & root) + class_tag* find_tag(const std::string & fullpath, class_tag & root, const config& match) { - return const_cast(const_cast(this)->find_tag(fullpath, root)); + return const_cast(const_cast(this)->find_tag(fullpath, root, match)); } void add_tags(const tag_map& list) @@ -508,10 +580,29 @@ class class_tag links_.insert(list.begin(), list.end()); } + void add_conditions(const condition_list& list) + { + conditions_.insert(conditions_.end(), list.begin(), list.end()); + } + /** Copies tags, keys and links of tag to this. */ void append_super(const class_tag& tag, const std::string& super); /** Expands all "super" copying their data to this. */ void expand(class_tag& root); }; + +extern template class class_tag::iterator; +extern template class class_tag::iterator; + +/** + * Stores information about a conditional portion of a tag. + * Format is the same as class_tag. + */ +class class_condition : public class_tag { + config filter_; +public: + class_condition(const config& info, const config& filter) : class_tag(info), filter_(filter) {} + bool matches(const config& cfg) const; +}; } diff --git a/src/serialization/validator.hpp b/src/serialization/validator.hpp index 4a424ff8e122..062d55ecc4b0 100644 --- a/src/serialization/validator.hpp +++ b/src/serialization/validator.hpp @@ -49,6 +49,7 @@ class abstract_validator * @param file Name of file */ virtual void open_tag(const std::string & name, + const config& parent, int start_line, const std::string &file, bool addittion = false) = 0;