If you set a duration based or infinite gameplay effect’s modifier to have a source/target “must have” tag requirement, it seems to cause the modifier to always not be included in the attribute’s evaluation. I looked into it and it seems the culprit is FAggregatorMod::UpdateQualifies when called by FActiveGameplayEffectsContainer::OnAttributeAggregatorDirty. When applying the duration/infinite gameplay effect, it gets called twice, but with different evaluation parameters passed in each time. The first time starts at FAttributeBasedFloat::CalculateMagnitude, where it fills out the EvaluationParameters param with the captured source and target tags. It goes through a few other functions and makes it to UpdateQualifies, but the mod’s tag requirements aren’t filled in yet.
When UpdateQualifies is called again, it starts at FActiveGameplayEffectsContainer::OnAttributeAggregatorDirty, where it creates and passes through an EvaluationParameters param with empty tag requirements. There’s a comment in this function that states the following:
"Our Aggregator has changed, we need to reevaluate this aggregator and update the current value of the attribute.
Note that this is not an execution, so there are no ‘source’ and ‘target’ tags to fill out in the FAggregatorEvaluateParameters.
ActiveGameplayEffects that have required owned tags will be turned on/off via delegates, and will add/remove themselves from attribute
aggregators when that happens."
Despite this, UpdateQualifies is run on the modifier with empty source and target tag parameters, and this time it’s own source and target tag requirements are filled out. So if there are any required tags, then either bSourceMet or bTargetMet will fail, and then IsQualified will be set to false, which will then cause the mod to not be included in the attribute’s evaluation.
Please let me know if this has already been fixed in 5.7 or 5.8.
Thanks for the detailed writeup — your analysis is correct. This is a real bug.
Ironically, the same function already handles a very similar case correctly — the AppliedSourceTagFilter/AppliedTargetTagFilter path self-resolves tags from the active GE handle when the caller doesn’t provide them. The requirements path just doesn’t do this.
Workarounds you can use now:
- Use GE-level OngoingTagRequirements instead of per-modifier tag requirements. Move your tag condition from the modifier’s Source/Target tags up to the GE’s OngoingTagRequirements. This uses the inhibition system to add/remove the entire GE from aggregators based on tag state, and it works correctly. The tradeoff is it’s all-or-nothing for the whole effect.
- Split into multiple GEs if you need different tag conditions on different modifiers. Give each GE one modifier and its own OngoingTagRequirements. This effectively gets you per-modifier conditional behavior through the working inhibition pathway.
- Modify the function locally: You could apply the changes in your local engine for the correct behavior you’re looking for.
void FAggregatorMod::UpdateQualifies(const FAggregatorEvaluateParameters& Parameters) const
{
static const FGameplayTagContainer EmptyTagContainer;
// --- CHANGED: resolve tags from the active GE handle when caller doesn't provide them ---
const FGameplayTagContainer* SrcTagsPtr = Parameters.SourceTags;
const FGameplayTagContainer* TgtTagsPtr = Parameters.TargetTags;
const UAbilitySystemComponent* HandleComponent = ActiveHandle.GetOwningAbilitySystemComponent();
if (HandleComponent)
{
if (!SrcTagsPtr)
SrcTagsPtr = HandleComponent->GetGameplayEffectSourceTagsFromHandle(ActiveHandle);
if (!TgtTagsPtr)
TgtTagsPtr = HandleComponent->GetGameplayEffectTargetTagsFromHandle(ActiveHandle);
}
const FGameplayTagContainer& SrcTags = SrcTagsPtr ? *SrcTagsPtr : EmptyTagContainer;
const FGameplayTagContainer& TgtTags = TgtTagsPtr ? *TgtTagsPtr : EmptyTagContainer;
// --- END CHANGED ---
This is the exact fix planned. However, because UpdateQualifies is called during every aggregator evaluation across all active Duration/Infinite effects, we need to vet this thoroughly. Games may have unknowingly shipped around this behavior, and changing qualification logic could cause modifiers to suddenly start applying where they previously didn’t. We’re going to look at this carefully — likely behind a CVar so projects can opt in — but I can’t commit to a specific release yet.