diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java index 58b3ebb4da..ffccd56353 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java @@ -343,4 +343,28 @@ public class ConvokeTest extends CardTestPlayerBaseWithAIHelps { assertPermanentCount(playerA, "Hogaak, Arisen Necropolis", 1); } + + @Test + public void test_Mana_MemoryOverflow() { + // possible bug: convoke mana calculation can overflow server's memory (too much mana options from too much permanents) + // https://github.com/magefree/mage/issues/6938 + + // Create X 1/1 white Soldier creature tokens with lifelink. + // Convoke (Your creatures can help cast this spell. Each creature you tap while casting this spell pays for {1} or one mana of that creature’s color.) + addCard(Zone.HAND, playerA, "March of the Multitudes", 1); // {X}{G}{W}{W} + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 500); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "March of the Multitudes"); + setChoice(playerA, "X=1"); + addTarget(playerA, "Grizzly Bears"); // convoke pay + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Soldier", 1); + } } \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/mana/ManaOptions.java b/Mage/src/main/java/mage/abilities/mana/ManaOptions.java index 68105ff096..fc8a7152cf 100644 --- a/Mage/src/main/java/mage/abilities/mana/ManaOptions.java +++ b/Mage/src/main/java/mage/abilities/mana/ManaOptions.java @@ -1,30 +1,27 @@ package mage.abilities.mana; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import mage.Mana; import mage.abilities.Ability; -import mage.abilities.costs.Cost; -import mage.abilities.costs.common.TapSourceCost; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.ManaEvent; import mage.players.Player; import org.apache.log4j.Logger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** - * * @author BetaSteward_at_googlemail.com - * + *

* this class is used to build a list of all possible mana combinations it can * be used to find all the ways to pay a mana cost or all the different mana * combinations available to a player - * + *

* TODO: Conditional Mana is not supported yet. The mana adding removes the * condition of conditional mana - * */ public class ManaOptions extends ArrayList { @@ -87,6 +84,8 @@ public class ManaOptions extends ArrayList { } } } + + forceManaDeduplication(); } private void addManaVariation(List netManas, ActivatedManaAbilityImpl ability, Game game) { @@ -102,6 +101,8 @@ public class ManaOptions extends ArrayList { } } } + + forceManaDeduplication(); } private static List> getSimulatedTriggeredManaFromPlayer(Game game, Ability ability) { @@ -189,9 +190,9 @@ public class ManaOptions extends ArrayList { if (startingMana.includesMana(manaCosts)) { // can pay the mana costs to use the ability if (!subtractCostAddMana(manaCosts, triggeredManaVariation, ability.getCosts().isEmpty(), startingMana)) { // the starting mana includes mana parts that the increased mana does not include, so add starting mana also as an option - add(prevMana); - } - wasUsable = true; + add(prevMana); + } + wasUsable = true; } else { // mana costs can't be paid so keep starting mana add(prevMana); @@ -251,10 +252,13 @@ public class ManaOptions extends ArrayList { } } } + if (this.size() > 30 || replaces > 30) { logger.trace("ManaOptionsCosts " + this.size() + " Ign:" + replaces + " => " + this.toString()); logger.trace("Abilities: " + abilities.toString()); } + forceManaDeduplication(); + return wasUsable; } @@ -302,6 +306,8 @@ public class ManaOptions extends ArrayList { } } } + + forceManaDeduplication(); } public void addMana(Mana addMana) { @@ -311,6 +317,8 @@ public class ManaOptions extends ArrayList { for (Mana mana : this) { mana.add(addMana); } + + forceManaDeduplication(); } public void addMana(ManaOptions options) { @@ -335,6 +343,17 @@ public class ManaOptions extends ArrayList { } } } + + forceManaDeduplication(); + } + + private void forceManaDeduplication() { + // memory overflow protection - force de-duplication on too much mana sources + // bug example: https://github.com/magefree/mage/issues/6938 + // use it after new mana adding + if (this.size() > 1000) { + this.removeDuplicated(); + } } public ManaOptions copy() { @@ -408,14 +427,16 @@ public class ManaOptions extends ArrayList { } } + + forceManaDeduplication(); + return oldManaWasReplaced; } /** - * - * @param number of generic mana + * @param number of generic mana * @param manaAvailable - * @return + * @return */ public static List getPossiblePayCombinations(int number, Mana manaAvailable) { List payCombinations = new ArrayList<>();