-
Notifications
You must be signed in to change notification settings - Fork 834
Refactor: clean filter logic #13713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Refactor: clean filter logic #13713
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine except few potential problems, see below
if (o instanceof Permanent) { | ||
return permanentFilter.match((Permanent) o, playerId, source, game); | ||
} else if (o instanceof Card) { | ||
return cardFilter.match((Card) o, playerId, source, game); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks buggy. New code do not filter object and apply it to all filters. Permanent is Card. So instead strict check in old code -- new multiple filters will check it for both. (it's ok for default MultiFilters
logic, but bad for more strict use cases like that).
Must re-check MultiFilters replace and override that method to be more strict
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check is actually done with the checkObjectClass
call in FilterImpl::match
.
So for the three current usage, FilterPermanent has a (object instanceof Permanent) check, FilterPlayer has a (object instanceof Player) check, and FilterCard has a (object instanceof Card) check.
For future-proofing, the intent is "do any of the filter match the object?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh right I get the Permanent is Card issue. I'll think how to solve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uhm maybe in the future, we would want to have FilterCard exclude Permanents?
That would need proper investigation if that breaks card functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For info: permanent extends card -- it's xmage only feature to share big amount of code between cards and permanents. It's not same by mtg rules. So strict permanent/card filters can be good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the general direction.
I thought of another idea though for after this- what if we refactored Predicate interface to always be four argument match itself? Then we'd have only one type of predicate, no need for the ObjectSourcePlayer wrapper. Could be much cleaner in many ways.
import mage.filter.predicate.Predicates; | ||
|
||
/** | ||
* // TODO: migrate all FilterObject to more specific ones, then remove this class? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to agree. I think right now it's only used for filters that implement a protection ability? Could the new FilterSource be used for that instead? Not sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a quick look, yes protection ability is mostly what is remaining. There are a few kind of protection that FilterSource will not match (protection from players/opponents). But maybe we just need to have a protection ability that can be made out of a FilterSource or a FilterPlayer?
And there are also a few odd uses like in SpliceAbility and SnowManaCost. Nothing that couldn't be refactored down the line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, ProtectionAbility is a mess of hacks checking instanceof the generic Filter and blindly groping to figure out how it should work. That's a whole separate thing to tackle and rework to a FilterSource for everything.
Under the rules, it should still be quite possible to implement as a FilterSource, with a new type of predicate that matches objects controlled by that player, with no need for a FilterPlayer at all.
A permanent or player with protection from a specific player has protection from each object that player controls and protection from each object that player owns not controlled by another player, regardless of that object's characteristic values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For info. From xmage side — protection logic implemented not only by “canTarget” (it’s a final destination for a check), but by “Ability source” param too. E.g. if it not target then send null instead source param. But not all choose dialogs use it. So it can be wrongly enabled/disabled in some choice dialogs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also some SBA do not have source info and send null in filters/dialogs. So it’s not about replace all x2 by x4 params call, but to find and fix/improve depended places too. x4 params usage must be a final goal, but it’s impossible without break/rework some hidden logic. Just try to use x4 as much as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the possible solution — introduce global/fake ability to such places. So it will call x4 params all the time, but source info will lead to non existing/system object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For info. From xmage side — protection logic implemented not only by “canTarget” (it’s a final destination for a check), but by “Ability source” param too. E.g. if it not target then send null instead source param. But not all choose dialogs use it. So it can be wrongly enabled/disabled in some choice dialogs.
Well, after some research -- many filters require source info and do not have null protection (search bad code by input.getSource().
). All that filters will fail/raise errors with AI games. So it's better to fill source info as much as possible (e.g. call 4th params methods). I'll remove null source logic from choose dialogs (example: choose and chooseTarget) and keep nullable source for SBA actions only (when it's impossible to find a real source).
So target's notTarget
field is the one and the final source of the "targetted" and "not targetted" info. Target::canTarget
methods must check protections by itself or call super
(it's already use that logic).
|
||
@Override | ||
public boolean match(Permanent permanent, UUID playerId, Ability source, Game game) { | ||
if (!this.match(permanent, game) || !permanent.isPhasedIn()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is that check for phasedIn ever relevant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, this is a concern for the Target
class to iterate on the non-phased objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Be careful with PhasedIn - they are poorly supported in xmage code and tests so it may be one of the few defenses here that everything rests on)))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Battlefield::getAllActivePermanents is the usual way to exclude phased out permanents.
Battlefield::getAllPermanents is the one that is not checking, and there is a bug on that every so often (but Phasing is not played that much, so it is not all that concerning.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for safety, I'm adding back the check, but I think we could do without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to tag #12121 for tracking.
I think this is the wrong place for phased in check in the engine. Unfortunately, after investigating, it's best to leave it in place for now.
Usages that already handle phased in check and don't need this check:
- Battlefield:: getActivePermanents, count, and similar
- TargetPermanent:: canChoose, possibleTargets
Usages that could be relying on this:
- TargetPermanent:: canTarget (though that shouldn't result in a bug in any case)
- Any effect that calls
getPermanent()
and then checks it against a filter
Only the 4-argument match had it before this rework, so no need to add it to 2-argument match. Alternatively, is it cleaner to check in checkObjectClass
instead of match
?
|
||
@Override | ||
public boolean checkObjectClass(Object object) { | ||
// TODO: investigate if we can/should exclude Permanent here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I vaguely recall that FilterCard is sometimes used for Spells. It's possible that it's (mis)used somewhere for Permanents as well, but I'm not really sure how to investigate
return super.match(stackObject, playerId, source, game); | ||
public boolean checkObjectClass(Object object) { | ||
return object instanceof Spell | ||
|| object instanceof Card; // TODO: investigate. Is sometimes used for checking a spell's characteristic before cast |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's just normal usage to check characteristics, as you often need to check before the Spell is actually created. also Spell implements Card interface. Not sure if there's really an action here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, I would like to FilterSpell to only work on Spells (for stuff like 'counter target creature spell' or 'Spells you control can't be countered'), and would introduce another class for effects that affect spells and cards about to be cast (like 'Nonartifact spells you cast have improvise' or 'You may cast a spell with mana value 4 or less from your hand without paying its mana cost.').
Maybe that is too much refactor work for too little gain. I find it a little weird to use a FilterSpell with a TargetCard though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could get behind that - distinction between a Spell on the stack, and something that could be cast. I'd want to see a detailed proposal for how it would work though. In any case, not in scope here
return predicates; | ||
} | ||
|
||
public List<ObjectSourcePlayerPredicate<E>> getExtraPredicates() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remember to add @Override
where appropriate
edbd847
to
d1f190a
Compare
(sorry, tried to address a merge conflict and screwed it up, should be fixed properly now) |
I do agree. We still have to carefully refactor a bunch of code to remove the use of the 2-argument match all over the codebase. |
This pass the unit tests.
I'm not that happy with the 2 overloads for add, and addExtra in addition, but that seemed like the easiest way to get through. Without that, the compiler was kinda lost:

Potential follow-ups: