Skip to content

Conversation

Susucre
Copy link
Contributor

@Susucre Susucre commented Jun 1, 2025

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:
image

Potential follow-ups:

  • move arround the Filter in filter/common to more specific folders for-permanent for-player for-stackobject, etc... Also move FilterOpponent, FilterPermanentThisOrAnother that are extending one of the type-specific FilterImpl, currently they are just in filter folder.
  • Not sure what to do of FilterMana, but it does not feel like it deserve to be called a Filter.
  • There is some weirdness of FilterSpell, that can be used on a card before it is cast. I'm not sure how to cleanup that properly going forward.
  • make use of 4-argument match in more places now that it is defined for all Filters. 2-argument match should be limited to a very specific usage.
  • I dislike the use of FilterObject, and think we can at term remove it in favor of specific Filters. You should know the general nature of the object you are filtering, or ignore implementation and use Filter if more generic.
  • FilterCard might want to exclude Permanent from checkObjectClass?

Copy link
Member

@JayDi85 JayDi85 left a 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);
Copy link
Member

@JayDi85 JayDi85 Jun 1, 2025

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

shot_250601_210846
shot_250601_210814

Copy link
Contributor Author

@Susucre Susucre Jun 1, 2025

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?"

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would want to have FilterCard exclude Permanents

It's can be good, but it can break current effects. I'm sure some shared effects can use card filters to apply to any objects in hand, stack and battlefield, e.g. adding additional types -- it must be checked manually:
shot_250601_214934

Copy link
Member

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.

Copy link
Contributor

@xenohedron xenohedron left a 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?
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

@JayDi85 JayDi85 Jun 14, 2025

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).

shot_250614_154426

shot_250614_154353


@Override
public boolean match(Permanent permanent, UUID playerId, Ability source, Game game) {
if (!this.match(permanent, game) || !permanent.isPhasedIn()) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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)))

Copy link
Contributor Author

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.)

Copy link
Contributor Author

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.

Copy link
Contributor

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.
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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() {
Copy link
Contributor

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

@xenohedron xenohedron force-pushed the refactor/clean-filter-logic branch from edbd847 to d1f190a Compare June 8, 2025 06:22
@xenohedron
Copy link
Contributor

(sorry, tried to address a merge conflict and screwed it up, should be fixed properly now)

@Susucre
Copy link
Contributor Author

Susucre commented Jun 8, 2025

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.

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.

@Susucre
Copy link
Contributor Author

Susucre commented Jun 8, 2025

So I'm not entirely sure the 2-argument match in FilterPermanent added with last commit is a good idea.
It caused some tests to fail, that I fixed in that commit with a few checkObjectClass before calls for the 2-argument match.

This is in the territory of hard to set right overloads with abstract types, so I'm not saying it can not be improved, but for now I do not see how to do it nicely. I'd like to have filters have a failsafe overload for any object, that does class checkObjectClass before attempting to match, but it is hard to set up.

Examples of tests that failed with ManaPool sourceFilters before adding checkObjectClass:
image

JayDi85 added a commit that referenced this pull request Jun 14, 2025
- fixed game errors with source related filters (related to #13713);
- fixed game freeze in hand's cards selection (related to #13290);
- fixed game freeze in target amount selection with X=0 (related to #13290);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants