Refactor: improved choose cards method to use source param (fixed NPE like #10233, #9974 and other bugs with choose cards)

This commit is contained in:
Oleg Agafonov 2023-04-21 11:02:57 +04:00
parent 5474787641
commit 689b93d005
280 changed files with 341 additions and 342 deletions

View file

@ -69,7 +69,7 @@ public class CastCardFromOutsideTheGameEffect extends OneShotEffect {
}
TargetCard target = new TargetCard(Zone.OUTSIDE, filterCard);
if (player.choose(Outcome.Benefit, filteredCards, target, game)) {
if (player.choose(Outcome.Benefit, filteredCards, target, source, game)) {
Card card = player.getSideboard().get(target.getFirstTarget(), game);
if (card != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);

View file

@ -52,7 +52,7 @@ public class DrawDiscardOneOfThemEffect extends OneShotEffect {
if (!drawnCards.isEmpty()) {
TargetCard cardToDiscard = new TargetCard(Zone.HAND, new FilterCard("card to discard"));
cardToDiscard.setNotTarget(true);
if (controller.choose(Outcome.Discard, drawnCards, cardToDiscard, game)) {
if (controller.choose(Outcome.Discard, drawnCards, cardToDiscard, source, game)) {
Card card = controller.getHand().get(cardToDiscard.getFirstTarget(), game);
if (card != null) {
return controller.discard(card, false, source, game);

View file

@ -41,7 +41,7 @@ public class ExileCardYouChooseTargetOpponentEffect extends OneShotEffect {
return true;
}
TargetCard target = new TargetCard(Zone.HAND, filter);
controller.choose(Outcome.Exile, opponent.getHand(), target, game);
controller.choose(Outcome.Exile, opponent.getHand(), target, source, game);
Card card = opponent.getHand().get(target.getFirstTarget(), game);
if (card != null) {
controller.moveCards(card, Zone.EXILED, source, game);

View file

@ -58,7 +58,7 @@ public class MillThenPutInHandEffect extends OneShotEffect {
return applyOtherwiseEffect(game, source);
}
TargetCard target = new TargetCard(0, 1, Zone.ALL, filter);
player.choose(Outcome.DrawCard, cards, target, game);
player.choose(Outcome.DrawCard, cards, target, source, game);
Card card = game.getCard(target.getFirstTarget());
if (card == null) {
return applyOtherwiseEffect(game, source);

View file

@ -123,7 +123,7 @@ public class PutCardFromOneOfTwoZonesOntoBattlefieldEffect extends OneShotEffect
default:
return false;
}
controller.choose(outcome, cards, targetCard, game);
controller.choose(outcome, cards, targetCard, source, game);
Card card = game.getCard(targetCard.getFirstTarget());
if (card == null || !controller.moveCards(card, Zone.BATTLEFIELD, source, game, tapped, false, false, null)) {
return false;

View file

@ -106,7 +106,7 @@ public class RevealAndSeparatePilesEffect extends OneShotEffect {
Player separatingPlayer = this.getExecutingPlayer(controller, game, source, playerWhoSeparates, "separate the revealed cards");
TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, filter);
List<Card> pile1 = new ArrayList<>();
separatingPlayer.choose(Outcome.Neutral, cards, target, game);
separatingPlayer.choose(Outcome.Neutral, cards, target, source, game);
target.getTargets()
.stream()
.map(game::getCard)

View file

@ -125,7 +125,7 @@ public class WishEffect extends OneShotEffect {
TargetCard target = new TargetCard(Zone.ALL, filter);
target.setNotTarget(true);
if (controller.choose(Outcome.Benefit, filteredCards, target, game)) {
if (controller.choose(Outcome.Benefit, filteredCards, target, source, game)) {
Card card = controller.getSideboard().get(target.getFirstTarget(), game);
if (card == null && alsoFromExile) {
card = game.getCard(target.getFirstTarget());

View file

@ -155,7 +155,7 @@ public class DiscardCardYouChooseTargetEffect extends OneShotEffect {
return result;
}
TargetCard target = new TargetCard(optional ? 0 : numberToDiscard, numberToDiscard, Zone.HAND, filter);
if (!controller.choose(Outcome.Benefit, revealedCards, target, game)) {
if (!controller.choose(Outcome.Benefit, revealedCards, target, source, game)) {
return result;
}
result = !player.discard(new CardsImpl(target.getTargets()), false, source, game).isEmpty();

View file

@ -71,7 +71,7 @@ public class SearchLibraryGraveyardPutInHandEffect extends OneShotEffect {
if (cardFound == null && controller.chooseUse(outcome, "Search your graveyard for a card named " + filter.getMessage() + '?', source, game)) {
TargetCard target = new TargetCardInYourGraveyard(0, 1, filter, true);
target.clearChosen();
if (controller.choose(outcome, controller.getGraveyard(), target, game)) {
if (controller.choose(outcome, controller.getGraveyard(), target, source, game)) {
if (!target.getTargets().isEmpty()) {
cardFound = game.getCard(target.getFirstTarget());
}

View file

@ -69,7 +69,7 @@ public class SearchLibraryGraveyardPutOntoBattlefieldEffect extends OneShotEffec
if (cardFound == null && controller.chooseUse(outcome, "Search your graveyard for a " + filter.getMessage() + '?', source, game)) {
TargetCard target = new TargetCardInYourGraveyard(0, 1, filter, true);
target.clearChosen();
if (controller.choose(outcome, controller.getGraveyard(), target, game)) {
if (controller.choose(outcome, controller.getGraveyard(), target, source, game)) {
if (!target.getTargets().isEmpty()) {
cardFound = game.getCard(target.getFirstTarget());
}

View file

@ -67,7 +67,7 @@ public class SearchLibraryGraveyardWithLessMVPutIntoPlay extends OneShotEffect {
if (cardFound == null && controller.chooseUse(outcome, "Search your graveyard for a " + filter.getMessage() + " with mana value X or less" + '?', source, game)) {
TargetCard target = new TargetCard(0, 1, Zone.GRAVEYARD, advancedFilter);
target.clearChosen();
if (controller.choose(outcome, controller.getGraveyard(), target, game)) {
if (controller.choose(outcome, controller.getGraveyard(), target, source, game)) {
if (!target.getTargets().isEmpty()) {
cardFound = game.getCard(target.getFirstTarget());
}

View file

@ -69,7 +69,7 @@ public abstract class SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
if (cardsCount > 0) {
filter.setMessage("card named " + cardName + " in the graveyard of " + targetPlayer.getName());
TargetCard target = new TargetCard((graveyardExileOptional ? 0 : cardsCount), cardsCount, Zone.GRAVEYARD, filter);
if (controller.choose(Outcome.Exile, targetPlayer.getGraveyard(), target, game)) {
if (controller.choose(Outcome.Exile, targetPlayer.getGraveyard(), target, source, game)) {
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
}
@ -78,7 +78,7 @@ public abstract class SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
cardsCount = (cardName.isEmpty() ? 0 : targetPlayer.getHand().count(filter, game));
filter.setMessage("card named " + cardName + " in the hand of " + targetPlayer.getName());
TargetCard target = new TargetCard(0, cardsCount, Zone.HAND, filter);
if (controller.choose(Outcome.Exile, targetPlayer.getHand(), target, game)) {
if (controller.choose(Outcome.Exile, targetPlayer.getHand(), target, source, game)) {
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
@ -88,7 +88,7 @@ public abstract class SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
cardsCount = (cardName.isEmpty() ? 0 : cardsInLibrary.count(filter, game));
filter.setMessage("card named " + cardName + " in the library of " + targetPlayer.getLogName());
TargetCardInLibrary targetLib = new TargetCardInLibrary(0, cardsCount, filter);
if (controller.choose(Outcome.Exile, cardsInLibrary, targetLib, game)) {
if (controller.choose(Outcome.Exile, cardsInLibrary, targetLib, source, game)) {
controller.moveCards(new CardsImpl(targetLib.getTargets()), Zone.EXILED, source, game);
}
targetPlayer.shuffleLibrary(source, game);

View file

@ -63,7 +63,7 @@ public class FatesealEffect extends OneShotEffect {
TargetCard target1 = new TargetCard(Zone.LIBRARY, filter1);
target1.setRequired(false);
// move cards to the bottom of the library
while (!cards.isEmpty() && controller.choose(Outcome.Detriment, cards, target1, game)) {
while (!cards.isEmpty() && controller.choose(Outcome.Detriment, cards, target1, source, game)) {
if (!controller.canRespond() || !opponent.canRespond()) {
return false;
}

View file

@ -144,7 +144,7 @@ class CascadeEffect extends OneShotEffect {
if (event.getAmount() > 0) {
TargetCardInExile target = new TargetCardInExile(0, event.getAmount(), StaticFilters.FILTER_CARD_LAND, null, true);
target.withChooseHint("land to put onto battlefield tapped");
controller.choose(Outcome.PutCardInPlay, cardsToExile, target, game);
controller.choose(Outcome.PutCardInPlay, cardsToExile, target, source, game);
controller.moveCards(
new CardsImpl(target.getTargets()).getCards(game), Zone.BATTLEFIELD,
source, game, true, false, false, null

View file

@ -93,7 +93,7 @@ class HideawayExileEffect extends OneShotEffect {
}
TargetCard target = new TargetCard(Zone.LIBRARY, filter);
target.setNotTarget(true);
controller.choose(Outcome.Detriment, cards, target, game);
controller.choose(Outcome.Detriment, cards, target, source, game);
Card card = cards.get(target.getFirstTarget(), game);
if (card != null) {
controller.moveCardsToExile(

View file

@ -101,7 +101,7 @@ class RippleEffect extends OneShotEffect {
target1.setRequired(false);
// choose cards to play for free
while (player.canRespond() && cards.count(sameNameFilter, game) > 0 && player.choose(Outcome.PlayForFree, cards, target1, game)) {
while (player.canRespond() && cards.count(sameNameFilter, game) > 0 && player.choose(Outcome.PlayForFree, cards, target1, source, game)) {
Card card = cards.get(target1.getFirstTarget(), game);
if (card != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);

View file

@ -416,7 +416,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
ZoneChangeEvent event = new ZoneChangeEvent(mainCard.getId(), ability, controllerId, fromZone, Zone.STACK);
Spell spell = new Spell(this, ability.getSpellAbilityToResolve(game), controllerId, event.getFromZone(), game);
ZoneChangeInfo.Stack info = new ZoneChangeInfo.Stack(event, spell);
return ZonesHandler.cast(info, game, ability);
return ZonesHandler.cast(info, ability, game);
}
@Override

View file

@ -24,9 +24,9 @@ import java.util.*;
*/
public final class ZonesHandler {
public static boolean cast(ZoneChangeInfo info, Game game, Ability source) {
public static boolean cast(ZoneChangeInfo info, Ability source, Game game) {
if (maybeRemoveFromSourceZone(info, game, source)) {
placeInDestinationZone(info, game, 0);
placeInDestinationZone(info,0, source, game);
// create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx))
Set<Card> cards = new HashSet<>();
Set<PermanentToken> tokens = new HashSet<>();
@ -54,10 +54,10 @@ public final class ZonesHandler {
public static boolean moveCard(ZoneChangeInfo info, Game game, Ability source) {
List<ZoneChangeInfo> list = new ArrayList<>();
list.add(info);
return !moveCards(list, game, source).isEmpty();
return !moveCards(list, source, game).isEmpty();
}
public static List<ZoneChangeInfo> moveCards(List<ZoneChangeInfo> zoneChangeInfos, Game game, Ability source) {
public static List<ZoneChangeInfo> moveCards(List<ZoneChangeInfo> zoneChangeInfos, Ability source, Game game) {
// Handle Unmelded Meld Cards
for (ListIterator<ZoneChangeInfo> itr = zoneChangeInfos.listIterator(); itr.hasNext(); ) {
ZoneChangeInfo info = itr.next();
@ -110,7 +110,7 @@ public final class ZonesHandler {
// All permanents go to battlefield at the same time (=create order)
createOrder = game.getState().getNextPermanentOrderNumber();
}
placeInDestinationZone(zoneChangeInfo, game, createOrder);
placeInDestinationZone(zoneChangeInfo, createOrder, source, game);
if (game.getPhase() != null) { // moving cards to zones before game started does not need events
game.addSimultaneousEvent(zoneChangeInfo.event);
}
@ -118,14 +118,14 @@ public final class ZonesHandler {
return zoneChangeInfos;
}
private static void placeInDestinationZone(ZoneChangeInfo info, Game game, int createOrder) {
private static void placeInDestinationZone(ZoneChangeInfo info, int createOrder, Ability source, Game game) {
// Handle unmelded cards
if (info instanceof ZoneChangeInfo.Unmelded) {
ZoneChangeInfo.Unmelded unmelded = (ZoneChangeInfo.Unmelded) info;
Zone toZone = null;
for (ZoneChangeInfo subInfo : unmelded.subInfo) {
toZone = subInfo.event.getToZone();
placeInDestinationZone(subInfo, game, createOrder);
placeInDestinationZone(subInfo, createOrder, source, game);
}
// We arbitrarily prefer the bottom half card. This should never be relevant.
if (toZone != null) {
@ -199,7 +199,8 @@ public final class ZonesHandler {
break;
case GRAVEYARD:
for (Card card : chooseOrder(
"order to put in graveyard (last chosen will be on top)", cardsToMove, owner, game)) {
"order to put in graveyard (last chosen will be on top)",
cardsToMove, owner, source, game)) {
game.getPlayer(card.getOwnerId()).getGraveyard().add(card);
}
break;
@ -207,13 +208,15 @@ public final class ZonesHandler {
if (info instanceof ZoneChangeInfo.Library && ((ZoneChangeInfo.Library) info).top) {
// on top
for (Card card : chooseOrder(
"order to put on top of library (last chosen will be topmost)", cardsToMove, owner, game)) {
"order to put on top of library (last chosen will be topmost)",
cardsToMove, owner, source, game)) {
game.getPlayer(card.getOwnerId()).getLibrary().putOnTop(card, game);
}
} else {
// on bottom
for (Card card : chooseOrder(
"order to put on bottom of library (last chosen will be bottommost)", cardsToMove, owner, game)) {
"order to put on bottom of library (last chosen will be bottommost)",
cardsToMove, owner, source, game)) {
game.getPlayer(card.getOwnerId()).getLibrary().putOnBottom(card, game);
}
}
@ -404,7 +407,7 @@ public final class ZonesHandler {
return success;
}
public static List<Card> chooseOrder(String message, Cards cards, Player player, Game game) {
public static List<Card> chooseOrder(String message, Cards cards, Player player, Ability source, Game game) {
List<Card> order = new ArrayList<>();
if (cards.isEmpty()) {
return order;
@ -412,7 +415,7 @@ public final class ZonesHandler {
TargetCard target = new TargetCard(Zone.ALL, new FilterCard(message));
target.setRequired(true);
while (player.canRespond() && cards.size() > 1) {
player.choose(Outcome.Neutral, cards, target, game);
player.choose(Outcome.Neutral, cards, target, source, game);
UUID targetObjectId = target.getFirstTarget();
order.add(cards.get(targetObjectId, game));
cards.remove(targetObjectId);

View file

@ -173,7 +173,7 @@ class MadWizardsLairEffect extends OneShotEffect {
}
player.revealCards(source, cards, game);
TargetCardInHand target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_NON_LAND);
player.choose(Outcome.PlayForFree, cards, target, game);
player.choose(Outcome.PlayForFree, cards, target, source, game);
Card card = player.getHand().get(target.getFirstTarget(), game);
if (card == null) {
return true;

View file

@ -153,7 +153,7 @@ class ThroneOfTheDeadThreeEffect extends OneShotEffect {
break;
default:
TargetCardInLibrary target = new TargetCardInLibrary(StaticFilters.FILTER_CARD_CREATURE);
player.choose(outcome, cards, target, game);
player.choose(outcome, cards, target, source, game);
card = cards.get(target.getFirstTarget(), game);
}
if (card != null) {

View file

@ -625,10 +625,6 @@ public interface Player extends MageItem, Copyable<Player> {
boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options);
// TODO: remove to use choose with "Ability source"
default boolean choose(Outcome outcome, Cards cards, TargetCard target, Game game) {
return choose(outcome, cards, target, null, game);
}
boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game);
boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game);

View file

@ -937,7 +937,7 @@ public abstract class PlayerImpl implements Player, Serializable {
new FilterCard("card ORDER to put on the BOTTOM of your library (last one chosen will be bottommost)"));
target.setRequired(true);
while (cards.size() > 1 && this.canRespond()
&& this.choose(Outcome.Neutral, cards, target, game)) {
&& this.choose(Outcome.Neutral, cards, target, source, game)) {
UUID targetObjectId = target.getFirstTarget();
if (targetObjectId == null) {
break;
@ -1031,7 +1031,7 @@ public abstract class PlayerImpl implements Player, Serializable {
target.setRequired(true);
while (cards.size() > 1
&& this.canRespond()
&& this.choose(Outcome.Neutral, cards, target, game)) {
&& this.choose(Outcome.Neutral, cards, target, source, game)) {
UUID targetObjectId = target.getFirstTarget();
if (targetObjectId == null) {
break;
@ -2691,7 +2691,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// handling Panglacial Wurm - cast cards while searching from own library
if (targetPlayer.getId().equals(searchingPlayer.getId())) {
if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) {
if (handleCastableCardsWhileLibrarySearching(library, targetPlayer, source, game)) {
// clear all choices to start from scratch (casted cards must be removed from library)
newTarget.clearChosen();
continue;
@ -2748,7 +2748,7 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) {
private boolean handleCastableCardsWhileLibrarySearching(Library library, Player targetPlayer, Ability source, Game game) {
// must return true after cast try (to restart searching process without casted cards)
// uses for handling Panglacial Wurm:
// * While you're searching your library, you may cast Panglacial Wurm from your library.
@ -2776,7 +2776,7 @@ public abstract class PlayerImpl implements Player, Serializable {
targetCard.setNotTarget(true);
while (castableCards.size() > 0) {
targetCard.clearChosen();
if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) {
if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, source, game)) {
break;
}
@ -4539,7 +4539,7 @@ public abstract class PlayerImpl implements Player, Serializable {
byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects);
infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source));
}
infoList = ZonesHandler.moveCards(infoList, game, source);
infoList = ZonesHandler.moveCards(infoList, source, game);
for (ZoneChangeInfo info : infoList) {
Permanent permanent = game.getPermanent(info.event.getTargetId());
if (permanent != null) {

View file

@ -1282,7 +1282,7 @@ public final class CardUtil {
Cards castableCards = new CardsImpl(cardMap.keySet());
TargetCard target = new TargetCard(0, 1, Zone.ALL, defaultFilter);
target.setNotTarget(true);
player.choose(Outcome.PlayForFree, castableCards, target, game);
player.choose(Outcome.PlayForFree, castableCards, target, source, game);
cardToCast = castableCards.get(target.getFirstTarget(), game);
}
if (cardToCast == null) {