Fix recursion ability count version (#9296)
I use the same method what in master for weapon special recursion.
This commit is contained in:
parent
20df59154d
commit
c5351fb998
6 changed files with 331 additions and 0 deletions
|
@ -0,0 +1,57 @@
|
||||||
|
#textdomain wesnoth-test
|
||||||
|
|
||||||
|
#####
|
||||||
|
# API(s) being tested: [filter_self]ability_id_active=
|
||||||
|
##
|
||||||
|
# Actions:
|
||||||
|
# Alice and Bob are both of type Test Melee Quintain.
|
||||||
|
# Give Alice a leadership_test_recursion ability, which is active only if it is itself active.
|
||||||
|
# Have Alice attack with his weapon.
|
||||||
|
##
|
||||||
|
# Expected end state:
|
||||||
|
# Deterministic end state, without crashing, but BROKE STRICT.
|
||||||
|
# All abilities are inactive.
|
||||||
|
# Bob takes 10 damage.
|
||||||
|
#####
|
||||||
|
{COMMON_KEEP_A_B_UNIT_TEST "filter_self_ability_cycle_recursion_by_id" (
|
||||||
|
[event]
|
||||||
|
name=start
|
||||||
|
|
||||||
|
[modify_unit]
|
||||||
|
[filter]
|
||||||
|
id=alice
|
||||||
|
[/filter]
|
||||||
|
[effect]
|
||||||
|
apply_to=new_ability
|
||||||
|
[abilities]
|
||||||
|
[leadership]
|
||||||
|
id=leadership_test_recursion
|
||||||
|
value=50
|
||||||
|
cumulative=no
|
||||||
|
affect_self=yes
|
||||||
|
[filter_self]
|
||||||
|
ability_id_active=leadership_test_recursion
|
||||||
|
[/filter_self]
|
||||||
|
[/leadership]
|
||||||
|
[/abilities]
|
||||||
|
[/effect]
|
||||||
|
[/modify_unit]
|
||||||
|
|
||||||
|
[test_do_attack_by_id]
|
||||||
|
attacker=alice
|
||||||
|
defender=bob
|
||||||
|
weapon=0
|
||||||
|
[/test_do_attack_by_id]
|
||||||
|
|
||||||
|
[store_unit]
|
||||||
|
[filter]
|
||||||
|
id=bob
|
||||||
|
[/filter]
|
||||||
|
variable=bob
|
||||||
|
[/store_unit]
|
||||||
|
|
||||||
|
{ASSERT ({VARIABLE_CONDITIONAL bob.hitpoints equals 90})}
|
||||||
|
|
||||||
|
{SUCCEED}
|
||||||
|
[/event]
|
||||||
|
) SIDE1_LEADER="Test Melee Quintain" SIDE2_LEADER="Test Melee Quintain"}
|
|
@ -0,0 +1,57 @@
|
||||||
|
#textdomain wesnoth-test
|
||||||
|
|
||||||
|
#####
|
||||||
|
# API(s) being tested: [filter]ability_id_active=
|
||||||
|
##
|
||||||
|
# Actions:
|
||||||
|
# Alice and Bob are both of type Test Melee Quintain.
|
||||||
|
# Give Alice and Bob a leadership_test_recursion ability, which is active only if it is itself active.
|
||||||
|
# Have Alice attack with his weapon.
|
||||||
|
##
|
||||||
|
# Expected end state:
|
||||||
|
# Deterministic end state, without crashing, but BROKE STRICT.
|
||||||
|
# All abilities are inactive.
|
||||||
|
# Bob takes 10 damage.
|
||||||
|
#####
|
||||||
|
{COMMON_KEEP_A_B_UNIT_TEST "ability_cycle_recursion_by_id" (
|
||||||
|
[event]
|
||||||
|
name=start
|
||||||
|
|
||||||
|
[modify_unit]
|
||||||
|
[filter]
|
||||||
|
id=alice
|
||||||
|
[/filter]
|
||||||
|
[effect]
|
||||||
|
apply_to=new_ability
|
||||||
|
[abilities]
|
||||||
|
[leadership]
|
||||||
|
id=leadership_test_recursion
|
||||||
|
value=50
|
||||||
|
cumulative=no
|
||||||
|
affect_self=yes
|
||||||
|
[filter]
|
||||||
|
ability_id_active=leadership_test_recursion
|
||||||
|
[/filter]
|
||||||
|
[/leadership]
|
||||||
|
[/abilities]
|
||||||
|
[/effect]
|
||||||
|
[/modify_unit]
|
||||||
|
|
||||||
|
[test_do_attack_by_id]
|
||||||
|
attacker=alice
|
||||||
|
defender=bob
|
||||||
|
weapon=0
|
||||||
|
[/test_do_attack_by_id]
|
||||||
|
|
||||||
|
[store_unit]
|
||||||
|
[filter]
|
||||||
|
id=bob
|
||||||
|
[/filter]
|
||||||
|
variable=bob
|
||||||
|
[/store_unit]
|
||||||
|
|
||||||
|
{ASSERT ({VARIABLE_CONDITIONAL bob.hitpoints equals 90})}
|
||||||
|
|
||||||
|
{SUCCEED}
|
||||||
|
[/event]
|
||||||
|
) SIDE1_LEADER="Test Melee Quintain" SIDE2_LEADER="Test Melee Quintain"}
|
|
@ -0,0 +1,61 @@
|
||||||
|
#textdomain wesnoth-test
|
||||||
|
|
||||||
|
#####
|
||||||
|
# API(s) being tested: [affect_adjacent][filter]ability_id_active=
|
||||||
|
##
|
||||||
|
# Actions:
|
||||||
|
# Alice and Bob are both of type Test Melee Quintain.
|
||||||
|
# Give Alice and Bob a leadership_test_recursion ability, which is active only if an adjacent unit also has it active.
|
||||||
|
# Have Alice attack with his weapon.
|
||||||
|
##
|
||||||
|
# Expected end state:
|
||||||
|
# Deterministic end state, without crashing, but BROKE STRICT.
|
||||||
|
# All abilities are inactive.
|
||||||
|
# Bob takes 10 damage.
|
||||||
|
#####
|
||||||
|
#####
|
||||||
|
{COMMON_KEEP_A_B_UNIT_TEST "adjacent_abilities_recursion_by_id" (
|
||||||
|
[event]
|
||||||
|
name=start
|
||||||
|
|
||||||
|
[modify_unit]
|
||||||
|
[filter]
|
||||||
|
[/filter]
|
||||||
|
[effect]
|
||||||
|
apply_to=new_ability
|
||||||
|
[abilities]
|
||||||
|
[leadership]
|
||||||
|
id=leadership_test_recursion
|
||||||
|
value=50
|
||||||
|
cumulative=no
|
||||||
|
affect_self=no
|
||||||
|
affect_allies=yes
|
||||||
|
affect_enemies=yes
|
||||||
|
[affect_adjacent]
|
||||||
|
[filter]
|
||||||
|
ability_id_active=leadership_test_recursion
|
||||||
|
[/filter]
|
||||||
|
[/affect_adjacent]
|
||||||
|
[/leadership]
|
||||||
|
[/abilities]
|
||||||
|
[/effect]
|
||||||
|
[/modify_unit]
|
||||||
|
|
||||||
|
[test_do_attack_by_id]
|
||||||
|
attacker=alice
|
||||||
|
defender=bob
|
||||||
|
weapon=0
|
||||||
|
[/test_do_attack_by_id]
|
||||||
|
|
||||||
|
[store_unit]
|
||||||
|
[filter]
|
||||||
|
id=bob
|
||||||
|
[/filter]
|
||||||
|
variable=bob
|
||||||
|
[/store_unit]
|
||||||
|
|
||||||
|
{ASSERT ({VARIABLE_CONDITIONAL bob.hitpoints equals 90})}
|
||||||
|
|
||||||
|
{SUCCEED}
|
||||||
|
[/event]
|
||||||
|
) SIDE1_LEADER="Test Melee Quintain" SIDE2_LEADER="Test Melee Quintain"}
|
|
@ -349,8 +349,99 @@ std::vector<std::tuple<std::string, t_string, t_string, t_string>> unit::ability
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
* Value of unit::num_recursion_ at which allocations of further recursion_guards fail. This
|
||||||
|
* value is used per unit.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* With the limit set to 2, all tests pass, but as the limit only affects cases that would otherwise
|
||||||
|
* lead to a crash, it seems reasonable to leave a little headroom for more complex logic.
|
||||||
|
*/
|
||||||
|
constexpr unsigned int UNIT_RECURSION_LIMIT = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
* Print "Recursion limit reached" log messages, including deduplication if the same problem has
|
||||||
|
* already been logged.
|
||||||
|
*/
|
||||||
|
void show_recursion_warning(const unit& unit, const config& filter) {
|
||||||
|
// This function is only called when an ability is checked for the second time
|
||||||
|
// filter has already been parsed multiple times, so I'm not trying to optimize the performance
|
||||||
|
// of this; it's merely to prevent the logs getting spammed. For example, each of
|
||||||
|
// four_cycle_recursion_branching and event_test_filter_attack_student_weapon_condition only log
|
||||||
|
// 3 unique messages, but without deduplication they'd log 1280 and 392 respectively.
|
||||||
|
static std::vector<std::tuple<std::string, std::string>> already_shown;
|
||||||
|
|
||||||
|
auto identifier = std::tuple<std::string, std::string>{unit.id(), filter.debug()};
|
||||||
|
if(utils::contains(already_shown, identifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view filter_text_view = std::get<1>(identifier);
|
||||||
|
utils::trim(filter_text_view);
|
||||||
|
ERR_NG << "Looped recursion error for unit '" << unit.id()
|
||||||
|
<< "' while checking ability '" << filter_text_view << "'";
|
||||||
|
|
||||||
|
// Arbitrary limit, just ensuring that having a huge number of specials causing recursion
|
||||||
|
// warnings can't lead to unbounded memory consumption here.
|
||||||
|
if(already_shown.size() > 100) {
|
||||||
|
already_shown.clear();
|
||||||
|
}
|
||||||
|
already_shown.push_back(std::move(identifier));
|
||||||
|
}
|
||||||
|
}//anonymous namespace
|
||||||
|
|
||||||
|
unit::recursion_guard unit::update_variables_recursion() const
|
||||||
|
{
|
||||||
|
if(num_recursion_ < UNIT_RECURSION_LIMIT) {
|
||||||
|
return recursion_guard(*this);
|
||||||
|
}
|
||||||
|
return recursion_guard();
|
||||||
|
}
|
||||||
|
|
||||||
|
unit::recursion_guard::recursion_guard() = default;
|
||||||
|
|
||||||
|
unit::recursion_guard::recursion_guard(const unit & u)
|
||||||
|
: parent(u.shared_from_this())
|
||||||
|
{
|
||||||
|
u.num_recursion_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit::recursion_guard::recursion_guard(unit::recursion_guard&& other)
|
||||||
|
{
|
||||||
|
std::swap(parent, other.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
unit::recursion_guard::operator bool() const {
|
||||||
|
return bool(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
unit::recursion_guard& unit::recursion_guard::operator=(unit::recursion_guard&& other)
|
||||||
|
{
|
||||||
|
assert(this != &other);
|
||||||
|
assert(!parent);
|
||||||
|
std::swap(parent, other.parent);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit::recursion_guard::~recursion_guard()
|
||||||
|
{
|
||||||
|
if(parent) {
|
||||||
|
assert(parent->num_recursion_ > 0);
|
||||||
|
parent->num_recursion_--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool unit::ability_active(const std::string& ability,const config& cfg,const map_location& loc) const
|
bool unit::ability_active(const std::string& ability,const config& cfg,const map_location& loc) const
|
||||||
{
|
{
|
||||||
|
unit::recursion_guard filter_lock;
|
||||||
|
filter_lock = update_variables_recursion();
|
||||||
|
if(!filter_lock) {
|
||||||
|
show_recursion_warning(*this, cfg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
bool illuminates = ability == "illuminates";
|
bool illuminates = ability == "illuminates";
|
||||||
|
|
||||||
if (auto afilter = cfg.optional_child("filter"))
|
if (auto afilter = cfg.optional_child("filter"))
|
||||||
|
@ -423,6 +514,14 @@ bool unit::ability_active(const std::string& ability,const config& cfg,const map
|
||||||
|
|
||||||
bool unit::ability_affects_adjacent(const std::string& ability, const config& cfg,int dir,const map_location& loc,const unit& from) const
|
bool unit::ability_affects_adjacent(const std::string& ability, const config& cfg,int dir,const map_location& loc,const unit& from) const
|
||||||
{
|
{
|
||||||
|
unit::recursion_guard adj_lock;
|
||||||
|
if(cfg.has_child("affect_adjacent")){
|
||||||
|
adj_lock = update_variables_recursion();
|
||||||
|
if(!adj_lock) {
|
||||||
|
show_recursion_warning(*this, cfg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool illuminates = ability == "illuminates";
|
bool illuminates = ability == "illuminates";
|
||||||
|
|
||||||
assert(dir >=0 && dir <= 5);
|
assert(dir >=0 && dir <= 5);
|
||||||
|
@ -451,6 +550,14 @@ bool unit::ability_affects_adjacent(const std::string& ability, const config& cf
|
||||||
bool unit::ability_affects_self(const std::string& ability,const config& cfg,const map_location& loc) const
|
bool unit::ability_affects_self(const std::string& ability,const config& cfg,const map_location& loc) const
|
||||||
{
|
{
|
||||||
auto filter = cfg.optional_child("filter_self");
|
auto filter = cfg.optional_child("filter_self");
|
||||||
|
unit::recursion_guard self_lock;
|
||||||
|
if(filter){
|
||||||
|
self_lock = update_variables_recursion();
|
||||||
|
if(!self_lock) {
|
||||||
|
show_recursion_warning(*this, cfg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool affect_self = cfg["affect_self"].to_bool(true);
|
bool affect_self = cfg["affect_self"].to_bool(true);
|
||||||
if (!filter || !affect_self) return affect_self;
|
if (!filter || !affect_self) return affect_self;
|
||||||
return unit_filter(vconfig(*filter)).set_use_flat_tod(ability == "illuminates").matches(*this, loc);
|
return unit_filter(vconfig(*filter)).set_use_flat_tod(ability == "illuminates").matches(*this, loc);
|
||||||
|
@ -1373,6 +1480,12 @@ namespace { // Helpers for attack_type::special_active()
|
||||||
if (!u) {
|
if (!u) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
//update and check variable_recursion for prevent check ability_id/type_active in case of infinite recursion.
|
||||||
|
unit::recursion_guard special_lock = (*u).update_variables_recursion();
|
||||||
|
if(!special_lock) {
|
||||||
|
show_recursion_warning(*u, filter);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
unit_filter ufilt{vconfig(*filter_child)};
|
unit_filter ufilt{vconfig(*filter_child)};
|
||||||
|
|
||||||
|
|
|
@ -1862,6 +1862,44 @@ public:
|
||||||
*/
|
*/
|
||||||
bool ability_matches_filter(const config & cfg, const std::string& tag_name, const config & filter) const;
|
bool ability_matches_filter(const config & cfg, const std::string& tag_name, const config & filter) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper similar to std::unique_lock for detecting when calculations such as abilities
|
||||||
|
* have entered infinite recursion.
|
||||||
|
*
|
||||||
|
* This assumes that there's only a single thread accessing the unit, it's a lightweight
|
||||||
|
* increment/decrement counter rather than a mutex.
|
||||||
|
*/
|
||||||
|
class recursion_guard {
|
||||||
|
friend class unit;
|
||||||
|
/**
|
||||||
|
* Only expected to be called in update_variables_recursion(), which handles some of the checks.
|
||||||
|
*/
|
||||||
|
explicit recursion_guard(const unit& u);
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Construct an empty instance, only useful for extending the lifetime of a
|
||||||
|
* recursion_guard returned from unit.update_variables_recursion() by
|
||||||
|
* std::moving it to an instance declared in a larger scope.
|
||||||
|
*/
|
||||||
|
explicit recursion_guard();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a level of recursion was available at the time when update_variables_recursion()
|
||||||
|
* created this object.
|
||||||
|
*/
|
||||||
|
operator bool() const;
|
||||||
|
|
||||||
|
recursion_guard(recursion_guard&& other);
|
||||||
|
recursion_guard(const recursion_guard& other) = delete;
|
||||||
|
recursion_guard& operator=(recursion_guard&&);
|
||||||
|
recursion_guard& operator=(const recursion_guard&) = delete;
|
||||||
|
~recursion_guard();
|
||||||
|
private:
|
||||||
|
std::shared_ptr<const unit> parent;
|
||||||
|
};
|
||||||
|
|
||||||
|
recursion_guard update_variables_recursion() const;
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
@ -2012,6 +2050,8 @@ private:
|
||||||
|
|
||||||
std::string role_;
|
std::string role_;
|
||||||
attack_list attacks_;
|
attack_list attacks_;
|
||||||
|
/** Number of instances of recursion_guard that are currently allocated permission to recurse */
|
||||||
|
mutable unsigned int num_recursion_ = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// TODO: I think we actually consider this to be part of the gamestate, so it might be better if it's not mutable,
|
// TODO: I think we actually consider this to be part of the gamestate, so it might be better if it's not mutable,
|
||||||
|
|
|
@ -383,6 +383,9 @@
|
||||||
9 four_cycle_recursion_branching
|
9 four_cycle_recursion_branching
|
||||||
9 four_cycle_recursion_by_id
|
9 four_cycle_recursion_by_id
|
||||||
9 four_cycle_recursion_by_tagname
|
9 four_cycle_recursion_by_tagname
|
||||||
|
9 ability_cycle_recursion_by_id
|
||||||
|
9 filter_self_ability_cycle_recursion_by_id
|
||||||
|
9 adjacent_abilities_recursion_by_id
|
||||||
0 negative_resistance_with_two_attack_types
|
0 negative_resistance_with_two_attack_types
|
||||||
0 positive_resistance_with_two_attack_types
|
0 positive_resistance_with_two_attack_types
|
||||||
0 taught_resistance_with_two_attack_types
|
0 taught_resistance_with_two_attack_types
|
||||||
|
|
Loading…
Add table
Reference in a new issue