Costs Tag Tracking part 2: Tag system and X values, reworked deep copy code (#11406)

* Implement Costs Tag Map system

* Use Costs Tag Map system to store X value for spells, abilities, and resolving permanents

* Store Bestow without target's tags
Change functions for getting tags and storing the tags of a new permanent

* Create and use deep copy function in CardUtil, add Copyable<T> to many classes

* Fix Hall Of the Bandit Lord infinite loop

* Add additional comments

* Don't store null/empty costs tags maps (saves memory)

* Fix two more Watchers with Ability variable

* Add check for exact collection types during deep copy

* Use generics instead of pure type erasure during deep copy

* convert more code to using deep copy helper, everything use Object copier, add EnumMap

* fix documentation

* Don't need the separate null checks anymore (handled in deepCopyObject)

* Minor cleanup
This commit is contained in:
ssk97 2023-11-16 11:12:32 -08:00 committed by GitHub
parent 72e30f1574
commit bea33c7493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 458 additions and 338 deletions

View file

@ -36,7 +36,7 @@ public final class Biophagus extends CardImpl {
Ability ability = new AnyColorManaAbility(new TapSourceCost(), true).withFlavorWord("Genomic Enhancement");
ability.getEffects().get(0).setText("Add one mana of any color. If this mana is spent to cast a creature spell, " +
"that creature enters the battlefield with an additional +1/+1 counter on it.");
this.addAbility(ability, new BiophagusWatcher(ability));
this.addAbility(ability, new BiophagusWatcher(ability.getId()));
}
private Biophagus(final Biophagus card) {
@ -51,11 +51,11 @@ public final class Biophagus extends CardImpl {
class BiophagusWatcher extends Watcher {
private final Ability source;
private final UUID sourceAbilityID;
BiophagusWatcher(Ability source) {
BiophagusWatcher(UUID sourceAbilityID) {
super(WatcherScope.CARD);
this.source = source;
this.sourceAbilityID = sourceAbilityID;
}
@Override
@ -68,7 +68,8 @@ class BiophagusWatcher extends Watcher {
&& event.getFlag()) {
if (target instanceof Spell) {
game.getState().addEffect(new BiophagusEntersBattlefieldEffect(
new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)), source);
new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)),
game.getAbility(sourceAbilityID, this.getSourceId()).orElse(null)); //null will cause an immediate crash
}
}
}

View file

@ -39,7 +39,7 @@ public final class GuildmagesForum extends CardImpl {
Ability ability = new AnyColorManaAbility(new GenericManaCost(1), true);
ability.getEffects().get(0).setText("Add one mana of any color. If that mana is spent on a multicolored creature spell, that creature enters the battlefield with an additional +1/+1 counter on it");
ability.addCost(new TapSourceCost());
this.addAbility(ability, new GuildmagesForumWatcher(ability));
this.addAbility(ability, new GuildmagesForumWatcher(ability.getId()));
}
private GuildmagesForum(final GuildmagesForum card) {
@ -54,11 +54,11 @@ public final class GuildmagesForum extends CardImpl {
class GuildmagesForumWatcher extends Watcher {
private final Ability source;
private final UUID sourceAbilityID;
GuildmagesForumWatcher(Ability source) {
GuildmagesForumWatcher(UUID sourceAbilityID) {
super(WatcherScope.CARD);
this.source = source;
this.sourceAbilityID = sourceAbilityID;
}
@Override
@ -71,7 +71,8 @@ class GuildmagesForumWatcher extends Watcher {
&& event.getFlag()) {
if (target instanceof Spell) {
game.getState().addEffect(new GuildmagesForumEntersBattlefieldEffect(
new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)), source);
new MageObjectReference(((Spell) target).getSourceId(), target.getZoneChangeCounter(game), game)),
game.getAbility(sourceAbilityID, this.getSourceId()).orElse(null)); //null will cause an immediate crash
}
}
}

View file

@ -2,6 +2,7 @@ package mage.cards.h;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import mage.MageObject;
import mage.Mana;
@ -45,7 +46,7 @@ public final class HallOfTheBanditLord extends CardImpl {
effect.setText("Add {C}. If that mana is spent on a creature spell, it gains haste");
Ability ability = new SimpleManaAbility(Zone.BATTLEFIELD, effect, new TapSourceCost());
ability.addCost(new PayLifeCost(3));
this.addAbility(ability, new HallOfTheBanditLordWatcher(ability));
this.addAbility(ability, new HallOfTheBanditLordWatcher(ability.getId()));
}
private HallOfTheBanditLord(final HallOfTheBanditLord card) {
@ -60,12 +61,12 @@ public final class HallOfTheBanditLord extends CardImpl {
class HallOfTheBanditLordWatcher extends Watcher {
private final Ability source;
private final UUID sourceAbilityID;
private final List<UUID> creatures = new ArrayList<>();
HallOfTheBanditLordWatcher(Ability source) {
HallOfTheBanditLordWatcher(UUID sourceAbilityID) {
super(WatcherScope.CARD);
this.source = source;
this.sourceAbilityID = sourceAbilityID;
}
@Override
@ -99,7 +100,7 @@ class HallOfTheBanditLordWatcher extends Watcher {
if (creatures.contains(event.getSourceId())) {
ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom);
effect.setTargetPointer(new FixedTarget(event.getSourceId(), game));
game.addEffect(effect, source);
game.addEffect(effect, game.getAbility(sourceAbilityID, this.getSourceId()).orElse(null)); //null will cause an immediate crash
creatures.remove(event.getSourceId());
}
}

View file

@ -10,6 +10,8 @@ import mage.cards.repository.CardRepository;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.PermanentToken;
import mage.util.CardUtil;
import org.junit.Assert;
import org.junit.Ignore;
@ -571,10 +573,10 @@ public class CopySpellTest extends CardTestPlayerBase {
}
@Test
public void test_CopiedSpellsHasntETB() {
public void test_CopiedSpellsETBCounters() {
// testing:
// - x in copied creature spell (copy x)
// - copied spells enters as tokens and it hasn't ETB, see rules below
// - copied spells enters as tokens and correctly ETB, see rules below
// 0/0
// Capricopian enters the battlefield with X +1/+1 counters on it.
@ -616,36 +618,34 @@ public class CopySpellTest extends CardTestPlayerBase {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Grenzo, Dungeon Warden", "Grenzo, Dungeon Warden");
// ETB triggers will not trigger here due not normal cast. From rules:
// - The token that a resolving copy of a spell becomes isnt said to have been created. (2021-04-16)
// - A nontoken permanent enters the battlefield when its moved onto the battlefield from another zone.
// A token enters the battlefield when its created. See rules 403.3, 603.6a, 603.6d, and 614.12.
//
// So both copies enters without counters:
// - Capricopian copy must die
// - Grenzo, Dungeon Warden must have default PT
// 608.3f If the object thats resolving is a copy of a permanent spell, it will become a token permanent
// as it is put onto the battlefield in any of the steps above.
// 111.12. A copy of a permanent spell becomes a token as it resolves. The token has the characteristics of
// the spell that became that token. The token is not created for the purposes of any replacement effects
// or triggered abilities that refer to creating a token.
// The tokens must enter with counters
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 1); // copy dies
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Capricopian", 2);
checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// counters checks
// counters checks, have to check if it's a card or a token since token copies have isCopy()=false
int originalCounters = currentGame.getBattlefield().getAllActivePermanents().stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> !p.isCopy())
.filter(p -> p instanceof PermanentCard)
.mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1))
.sum();
int copyCounters = currentGame.getBattlefield().getAllActivePermanents().stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> p.isCopy())
.filter(p -> p instanceof PermanentToken)
.mapToInt(p -> p.getCounters(currentGame).getCount(CounterType.P1P1))
.sum();
Assert.assertEquals("original grenzo must have 2x counters", 2, originalCounters);
Assert.assertEquals("copied grenzo must have 0x counters", 0, copyCounters);
Assert.assertEquals("copied grenzo must have 2x counters", 2, copyCounters);
}
@Test
@ -748,7 +748,6 @@ public class CopySpellTest extends CardTestPlayerBase {
* Thieving Skydiver is kicked and then copied, but the copied version does not let you gain control of anything.
*/
@Test
@Ignore
public void copySpellWithKicker() {
// When Thieving Skydiver enters the battlefield, if it was kicked, gain control of target artifact with mana value X or less.
// If that artifact is an Equipment, attach it to Thieving Skydiver.
@ -758,7 +757,8 @@ public class CopySpellTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3); // Original price, + 1 kicker, + 1 for Double Major
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerB, "Sol Ring", 2);
addCard(Zone.BATTLEFIELD, playerB, "Sol Ring", 1);
addCard(Zone.BATTLEFIELD, playerB, "Expedition Map", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Thieving Skydiver");
@ -766,14 +766,16 @@ public class CopySpellTest extends CardTestPlayerBase {
setChoice(playerA, "X=1");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Thieving Skydiver", "Thieving Skydiver");
addTarget(playerA, "Sol Ring"); // Choice for copy
addTarget(playerA, "Sol Ring"); // Choice for original
addTarget(playerA, "Expedition Map"); // Choice for original
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Sol Ring", 2); // 1 taken by original, one by copy
assertPermanentCount(playerA, "Sol Ring", 1);
assertPermanentCount(playerA, "Expedition Map", 1);
assertPermanentCount(playerB, "Sol Ring", 0);
assertPermanentCount(playerB, "Expedition Map", 0);
}
private void abilitySourceMustBeSame(Card card, String infoPrefix) {

View file

@ -65,7 +65,7 @@ public class CardIconsTest extends CardTestPlayerBase {
}
@Test
public void test_CostX_Copies() {
public void test_CostX_StackCopy() {
// Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it.
addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
@ -144,6 +144,67 @@ public class CardIconsTest extends CardTestPlayerBase {
.orElse(null);
Assert.assertNotNull("copied card must be in battlefield", copiedCardView);
Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size());
Assert.assertEquals("copied x cost text", "x=2", copiedCardView.getCardIcons().get(0).getText());
});
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
}
@Test
public void test_CostX_TokenCopy() {
//Legend Rule doesn't apply
addCard(Zone.BATTLEFIELD, playerA, "Mirror Gallery", 1);
// Grenzo, Dungeon Warden enters the battlefield with X +1/+1 counters on it.
addCard(Zone.HAND, playerA, "Grenzo, Dungeon Warden", 1);// {X}{B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
// Create a token that's a copy of target creature you control.
// should not copy the X value of the Grenzo
addCard(Zone.HAND, playerA, "Quasiduplicate", 1); // {1}{U}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
// cast Grenzo
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 2);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden");
setChoice(playerA, "X=2");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// cast Quasiduplicate
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Quasiduplicate", "Grenzo, Dungeon Warden");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grenzo, Dungeon Warden", 2);
// battlefield (card and copied card as token)
runCode("card icons in battlefield (cloned)", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
GameView gameView = getGameView(player);
PlayerView playerView = gameView.getPlayers().get(0);
Assert.assertEquals("player", player.getName(), playerView.getName());
// original
CardView originalCardView = playerView.getBattlefield().values()
.stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> !p.isToken())
.findFirst()
.orElse(null);
Assert.assertNotNull("original card must be in battlefield", originalCardView);
Assert.assertEquals("original must have x cost card icons", 1, originalCardView.getCardIcons().size());
Assert.assertEquals("original x cost text", "x=2", originalCardView.getCardIcons().get(0).getText());
//
CardView copiedCardView = playerView.getBattlefield().values()
.stream()
.filter(p -> p.getName().equals("Grenzo, Dungeon Warden"))
.filter(p -> p.isToken())
.findFirst()
.orElse(null);
Assert.assertNotNull("copied card must be in battlefield", copiedCardView);
Assert.assertEquals("copied must have x cost card icons", 1, copiedCardView.getCardIcons().size());
Assert.assertEquals("copied x cost text", "x=0", copiedCardView.getCardIcons().get(0).getText());
});

View file

@ -26,6 +26,7 @@ import mage.watchers.Watcher;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
@ -157,6 +158,26 @@ public interface Ability extends Controllable, Serializable {
void addManaCostsToPay(ManaCost manaCost);
/**
* Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet
* does NOT return the source permanent's tags
*
* @return The map of tags and corresponding objects
*/
Map<String, Object> getCostsTagMap();
/**
* Set tag to the value, initializes this ability's tags map if it is null
*/
void setCostsTag(String tag, Object value);
/**
* Returns the value of the tag or defaultValue if the tag is not found in this ability's tag map
* does NOT check the source permanent's tags, use CardUtil.getSourceCostsTag for that
*
* @return The given tag value (or the default if not found)
*/
Object getCostsTagOrDefault(String tag, Object defaultValue);
/**
* Retrieves the effects that are put into the place by the resolution of
* this ability.

View file

@ -83,6 +83,7 @@ public abstract class AbilityImpl implements Ability {
protected MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher)
protected String appendToRule = null;
protected int sourcePermanentTransformCount = 0;
private Map<String, Object> costsTagMap = null;
protected AbilityImpl(AbilityType abilityType, Zone zone) {
this.id = UUID.randomUUID();
@ -107,16 +108,9 @@ public abstract class AbilityImpl implements Ability {
this.manaCosts = ability.manaCosts.copy();
this.manaCostsToPay = ability.manaCostsToPay.copy();
this.costs = ability.costs.copy();
for (Watcher watcher : ability.getWatchers()) {
watchers.add(watcher.copy());
}
this.watchers = CardUtil.deepCopyObject(ability.getWatchers());
if (ability.subAbilities != null) {
this.subAbilities = new ArrayList<>();
for (Ability subAbility : ability.subAbilities) {
subAbilities.add(subAbility.copy());
}
}
this.subAbilities = CardUtil.deepCopyObject(ability.subAbilities);
this.modes = ability.getModes().copy();
this.ruleAtTheTop = ability.ruleAtTheTop;
this.ruleVisible = ability.ruleVisible;
@ -129,17 +123,14 @@ public abstract class AbilityImpl implements Ability {
this.canFizzle = ability.canFizzle;
this.targetAdjuster = ability.targetAdjuster;
this.costAdjuster = ability.costAdjuster;
for (Hint hint : ability.getHints()) {
this.hints.add(hint.copy());
}
for (CardIcon icon : ability.getIcons()) {
this.icons.add(icon.copy());
}
this.hints = CardUtil.deepCopyObject(ability.getHints());
this.icons = CardUtil.deepCopyObject(ability.getIcons());
this.customOutcome = ability.customOutcome;
this.identifier = ability.identifier;
this.activated = ability.activated;
this.appendToRule = ability.appendToRule;
this.sourcePermanentTransformCount = ability.sourcePermanentTransformCount;
this.costsTagMap = CardUtil.deepCopyObject(ability.costsTagMap);
}
@Override
@ -527,6 +518,7 @@ public abstract class AbilityImpl implements Ability {
((Cost) variableCost).setPaid();
String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')';
announceString.append(message);
setCostsTag("X",xValue);
}
}
return announceString.toString();
@ -631,6 +623,7 @@ public abstract class AbilityImpl implements Ability {
}
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana);
setCostsTag("X",xValue * xValueMultiplier);
}
variableManaCost.setPaid();
}
@ -713,6 +706,28 @@ public abstract class AbilityImpl implements Ability {
return manaCostsToPay;
}
/**
* Accessed to see what was optional/variable costs were paid
*
* @return
*/
@Override
public Map<String, Object> getCostsTagMap() {
return costsTagMap;
}
public void setCostsTag(String tag, Object value){
if (costsTagMap == null){
costsTagMap = new HashMap<>();
}
costsTagMap.put(tag, value);
}
public Object getCostsTagOrDefault(String tag, Object defaultValue){
if (costsTagMap != null && costsTagMap.containsKey(tag)){
return costsTagMap.get(tag);
}
return defaultValue;
}
@Override
public Effects getEffects() {
return getModes().getMode().getEffects();

View file

@ -1,12 +1,10 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.costs.OptionalAdditionalCostImpl;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.keyword.KickerAbility;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.util.CardUtil;
/**
@ -19,35 +17,9 @@ public enum GetKickerXValue implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
// calcs only kicker with X values
// kicker adds additional costs to spell ability
// only one X value per card possible
// kicker can be calls multiple times (use getKickedCounter)
int countX = 0;
Spell spell = game.getSpellOrLKIStack(sourceAbility.getSourceId());
if (spell != null && spell.getSpellAbility() != null) {
int xValue = spell.getSpellAbility().getManaCostsToPay().getX();
for (Ability ability : spell.getAbilities()) {
if (ability instanceof KickerAbility) {
// search that kicker used X value
KickerAbility kickerAbility = (KickerAbility) ability;
boolean haveVarCost = kickerAbility.getKickerCosts()
.stream()
.anyMatch(varCost -> !((OptionalAdditionalCostImpl) varCost).getVariableCosts().isEmpty());
if (haveVarCost) {
int kickedCount = ((KickerAbility) ability).getKickedCounter(game, sourceAbility);
if (kickedCount > 0) {
countX += kickedCount * xValue;
}
}
}
}
}
return countX;
// Currently identical logic to the Manacost X value
// which should be fine since you can only have one X at a time
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
}
@Override

View file

@ -1,11 +1,10 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.common.PayVariableLoyaltyCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.util.CardUtil;
/**
* @author TheElk801
@ -15,12 +14,7 @@ public enum GetXLoyaltyValue implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
for (Cost cost : sourceAbility.getCosts()) {
if (cost instanceof PayVariableLoyaltyCost) {
return ((PayVariableLoyaltyCost) cost).getAmount();
}
}
return 0;
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
}
@Override

View file

@ -1,10 +1,10 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.costs.VariableCost;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.util.CardUtil;
/**
* @author BetaSteward_at_googlemail.com
@ -14,12 +14,7 @@ public enum GetXValue implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return sourceAbility
.getCosts()
.getVariableCosts()
.stream()
.mapToInt(VariableCost::getAmount)
.sum();
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
}
@Override

View file

@ -4,10 +4,11 @@ import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.watchers.common.ManaSpentToCastWatcher;
import mage.util.CardUtil;
public enum ManacostVariableValue implements DynamicValue {
//TODO: all three of these variants plus GetXValue, GetKickerXValue, and GetXLoyaltyValue use the same logic
// and should be consolidated into a single instance
REGULAR, // if you need X on cast/activate (in stack) - reset each turn
ETB, // if you need X after ETB (in battlefield) - reset each turn
END_GAME; // if you need X until end game - keep data forever
@ -15,18 +16,7 @@ public enum ManacostVariableValue implements DynamicValue {
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
if (this == REGULAR) {
return sourceAbility.getManaCostsToPay().getX();
}
ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class);
if (watcher != null) {
if (this == END_GAME) {
return watcher.getLastXValue(sourceAbility, true);
} else {
return watcher.getLastXValue(sourceAbility, false);
}
}
return 0;
return (int) CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0);
}
@Override

View file

@ -1,8 +1,6 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.effects.EntersBattlefieldEffect;
import mage.abilities.effects.OneShotEffect;
import mage.constants.AbilityType;
import mage.constants.Outcome;
@ -59,19 +57,12 @@ public class EntersBattlefieldWithXCountersEffect extends OneShotEffect {
}
}
if (permanent != null) {
SpellAbility spellAbility = (SpellAbility) getValue(EntersBattlefieldEffect.SOURCE_CAST_SPELL_ABILITY);
if (spellAbility != null
&& spellAbility.getSourceId().equals(source.getSourceId())
&& permanent.getZoneChangeCounter(game) == spellAbility.getSourceObjectZoneChangeCounter()) {
if (spellAbility.getSourceId().equals(source.getSourceId())) { // put into play by normal cast
int amount = spellAbility.getManaCostsToPay().getX() * this.multiplier;
if (amount > 0) {
Counter counterToAdd = counter.copy();
counterToAdd.add(amount - counter.getCount());
List<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects);
}
}
int amount = ((int) CardUtil.getSourceCostsTag(game, source, "X", 0)) * multiplier;
if (amount > 0) {
Counter counterToAdd = counter.copy();
counterToAdd.add(amount - counter.getCount());
List<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
permanent.addCounters(counterToAdd, source.getControllerId(), source, game, appliedEffects);
}
}
return true;

View file

@ -2,13 +2,14 @@ package mage.abilities.hint;
import mage.abilities.Ability;
import mage.game.Game;
import mage.util.Copyable;
import java.io.Serializable;
/**
* @author JayDi85
*/
public interface Hint extends Serializable {
public interface Hint extends Serializable, Copyable<Hint> {
// It's a constant hint for cards/permanents (e.g. visible all the time)
//

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.util.Copyable;
import java.io.Serializable;
import java.util.Collection;
@ -11,7 +12,7 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
public interface Cards extends Set<UUID>, Serializable {
public interface Cards extends Set<UUID>, Serializable, Copyable<Cards> {
/**
* Add the passed in card to the set if it's not null.

View file

@ -1,6 +1,7 @@
package mage.counters;
import mage.util.CardUtil;
import mage.util.Copyable;
import java.io.Serializable;
@ -9,7 +10,7 @@ import org.apache.log4j.Logger;
/**
* @author BetaSteward_at_googlemail.com
*/
public class Counter implements Serializable {
public class Counter implements Serializable, Copyable<Counter> {
private static final Logger logger = Logger.getLogger(Counter.class);

View file

@ -1,6 +1,8 @@
package mage.counters;
import mage.util.Copyable;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
@ -10,7 +12,7 @@ import java.util.stream.Collectors;
/**
* @author BetaSteward_at_googlemail.com
*/
public class Counters extends HashMap<String, Counter> implements Serializable {
public class Counters extends HashMap<String, Counter> implements Serializable, Copyable<Counters> {
public Counters() {
}

View file

@ -2,6 +2,7 @@ package mage.filter;
import mage.filter.predicate.Predicate;
import mage.game.Game;
import mage.util.Copyable;
import java.io.Serializable;
import java.util.List;
@ -11,7 +12,7 @@ import java.util.List;
* @author BetaSteward_at_googlemail.com
* @author North
*/
public interface Filter<E> extends Serializable {
public interface Filter<E> extends Serializable, Copyable<Filter<E>> {
enum ComparisonScope {
Any, All

View file

@ -2,6 +2,7 @@ package mage.game;
import mage.MageItem;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.abilities.DelayedTriggeredAbility;
@ -118,6 +119,12 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Map<UUID, Permanent> getPermanentsEntering();
Map<Zone, Map<UUID, MageObject>> getLKI();
Map<MageObjectReference, Map<String, Object>> getPermanentCostsTags();
/**
* Take the source's Costs Tags and store it for later access through the MOR.
*/
void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source);
// Result must be checked for null. Possible errors search pattern: (\S*) = game.getCard.+\n(?!.+\1 != null)
Card getCard(UUID cardId);

View file

@ -2,6 +2,7 @@ package mage.game;
import mage.MageException;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.*;
import mage.abilities.common.AttachableToRestrictedAbility;
import mage.abilities.common.CantHaveMoreThanAmountCountersSourceAbility;
@ -184,48 +185,16 @@ public abstract class GameImpl implements Game {
//this.tableEventSource = game.tableEventSource; // client-server part, not need on copy/simulations
//this.playerQueryEventSource = game.playerQueryEventSource; // client-server part, not need on copy/simulations
for (Entry<UUID, Card> entry : game.gameCards.entrySet()) {
this.gameCards.put(entry.getKey(), entry.getValue().copy());
}
for (Entry<UUID, MeldCard> entry : game.meldCards.entrySet()) {
this.meldCards.put(entry.getKey(), entry.getValue().copy());
}
this.gameCards = CardUtil.deepCopyObject(game.gameCards);
this.meldCards = CardUtil.deepCopyObject(game.meldCards);
// lki
for (Entry<Zone, Map<UUID, MageObject>> entry : game.lki.entrySet()) {
Map<UUID, MageObject> lkiMap = new HashMap<>();
for (Entry<UUID, MageObject> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lki.put(entry.getKey(), lkiMap);
}
// lkiCardState
for (Entry<Zone, Map<UUID, CardState>> entry : game.lkiCardState.entrySet()) {
Map<UUID, CardState> lkiMap = new HashMap<>();
for (Entry<UUID, CardState> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lkiCardState.put(entry.getKey(), lkiMap);
}
// lkiExtended
for (Entry<UUID, Map<Integer, MageObject>> entry : game.lkiExtended.entrySet()) {
Map<Integer, MageObject> lkiMap = new HashMap<>();
for (Entry<Integer, MageObject> entryMap : entry.getValue().entrySet()) {
lkiMap.put(entryMap.getKey(), entryMap.getValue().copy());
}
this.lkiExtended.put(entry.getKey(), lkiMap);
}
// lkiShortLiving
for (Entry<Zone, Set<UUID>> entry : game.lkiShortLiving.entrySet()) {
this.lkiShortLiving.put(entry.getKey(), new HashSet<>(entry.getValue()));
}
this.lki = CardUtil.deepCopyObject(game.lki);
this.lkiCardState = CardUtil.deepCopyObject(game.lkiCardState);
this.lkiExtended = CardUtil.deepCopyObject(game.lkiExtended);
this.lkiShortLiving = CardUtil.deepCopyObject(game.lkiShortLiving);
for (Entry<UUID, Permanent> entry : game.permanentsEntering.entrySet()) {
this.permanentsEntering.put(entry.getKey(), entry.getValue().copy());
}
for (Entry<UUID, Counters> entry : game.enterWithCounters.entrySet()) {
this.enterWithCounters.put(entry.getKey(), entry.getValue().copy());
}
this.permanentsEntering = CardUtil.deepCopyObject(game.permanentsEntering);
this.enterWithCounters = CardUtil.deepCopyObject(game.enterWithCounters);
this.state = game.state.copy();
// client-server part, not need on copy/simulations:
@ -1451,6 +1420,10 @@ public abstract class GameImpl implements Game {
player.endOfTurn(this);
}
state.resetWatchers();
// Could be done any time as long as the stack is empty
// Tags are stored in the game state as a spell resolves into a permanent
// and must be kept while any abilities with that permanent as a source could resolve
state.cleanupPermanentCostsTags(this);
}
protected UUID pickChoosingPlayer() {
@ -3560,6 +3533,15 @@ public abstract class GameImpl implements Game {
return lki;
}
@Override
public Map<MageObjectReference, Map<String, Object>> getPermanentCostsTags() {
return state.getPermanentCostsTags();
}
@Override
public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){
state.storePermanentCostsTags(permanentMOR, source);
}
@Override
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command) {
// fake test ability for triggers and events

View file

@ -101,6 +101,7 @@ public class GameState implements Serializable, Copyable<GameState> {
private Map<UUID, Zone> zones = new HashMap<>();
private List<GameEvent> simultaneousEvents = new ArrayList<>();
private Map<UUID, CardState> cardState = new HashMap<>();
private Map<MageObjectReference, Map<String, Object>> permanentCostsTags = new HashMap<>(); // Permanent reference -> map of (tag -> values) describing how the permanent's spell was cast
private Map<UUID, MageObjectAttribute> mageObjectAttribute = new HashMap<>();
private Map<UUID, Integer> zoneChangeCounter = new HashMap<>();
private Map<UUID, Card> copiedCards = new HashMap<>();
@ -162,36 +163,19 @@ public class GameState implements Serializable, Copyable<GameState> {
this.stepNum = state.stepNum;
this.extraTurnId = state.extraTurnId;
this.effects = state.effects.copy();
for (TriggeredAbility trigger : state.triggered) {
this.triggered.add(trigger.copy());
}
this.triggered = CardUtil.deepCopyObject(state.triggered);
this.triggers = state.triggers.copy();
this.delayed = state.delayed.copy();
this.specialActions = state.specialActions.copy();
this.combat = state.combat.copy();
this.turnMods = state.turnMods.copy();
this.watchers = state.watchers.copy();
for (Map.Entry<String, Object> entry : state.values.entrySet()) {
if (entry.getValue() instanceof HashSet) {
this.values.put(entry.getKey(), ((HashSet) entry.getValue()).clone());
} else if (entry.getValue() instanceof EnumSet) {
this.values.put(entry.getKey(), ((EnumSet) entry.getValue()).clone());
} else if (entry.getValue() instanceof HashMap) {
this.values.put(entry.getKey(), ((HashMap) entry.getValue()).clone());
} else if (entry.getValue() instanceof List) {
this.values.put(entry.getKey(), ((List) entry.getValue()).stream().collect(Collectors.toList()));
} else {
this.values.put(entry.getKey(), entry.getValue());
}
}
this.values = CardUtil.deepCopyObject(state.values);
this.zones.putAll(state.zones);
this.simultaneousEvents.addAll(state.simultaneousEvents);
for (Map.Entry<UUID, CardState> entry : state.cardState.entrySet()) {
cardState.put(entry.getKey(), entry.getValue().copy());
}
for (Map.Entry<UUID, MageObjectAttribute> entry : state.mageObjectAttribute.entrySet()) {
mageObjectAttribute.put(entry.getKey(), entry.getValue().copy());
}
this.cardState = CardUtil.deepCopyObject(state.cardState);
this.permanentCostsTags = CardUtil.deepCopyObject(state.permanentCostsTags);
this.mageObjectAttribute = CardUtil.deepCopyObject(state.mageObjectAttribute);
this.zoneChangeCounter.putAll(state.zoneChangeCounter);
this.copiedCards.putAll(state.copiedCards);
this.permanentOrderNumber = state.permanentOrderNumber;
@ -231,6 +215,7 @@ public class GameState implements Serializable, Copyable<GameState> {
gameOver = false;
specialActions.clear();
cardState.clear();
permanentCostsTags.clear();
combat.clear();
turnMods.clear();
watchers.clear();
@ -280,6 +265,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.zones = state.zones;
this.simultaneousEvents = state.simultaneousEvents;
this.cardState = state.cardState;
this.permanentCostsTags = state.permanentCostsTags;
this.mageObjectAttribute = state.mageObjectAttribute;
this.zoneChangeCounter = state.zoneChangeCounter;
this.copiedCards = state.copiedCards;
@ -1369,6 +1355,29 @@ public class GameState implements Serializable, Copyable<GameState> {
return mageObjectAtt;
}
public Map<MageObjectReference, Map<String, Object>> getPermanentCostsTags() {
return permanentCostsTags;
}
/**
* Store the tags of source ability using the MOR as a reference
*/
void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){
if (source.getCostsTagMap() != null) {
permanentCostsTags.put(permanentMOR, CardUtil.deepCopyObject(source.getCostsTagMap()));
}
}
/**
* Removes the cost tags if the corresponding permanent is no longer on the battlefield.
* Only use if the stack is empty and nothing can refer to them anymore (such as at EOT, the current behavior)
*/
public void cleanupPermanentCostsTags(Game game){
getPermanentCostsTags().entrySet().removeIf(entry ->
!(entry.getKey().zoneCounterIsCurrent(game))
);
}
public void addWatcher(Watcher watcher) {
this.watchers.add(watcher);
}

View file

@ -4,6 +4,7 @@ import mage.MageObject;
import mage.ObjectColor;
import mage.constants.CardType;
import mage.constants.SuperType;
import mage.util.Copyable;
import mage.util.SubTypes;
import java.io.Serializable;
@ -16,7 +17,7 @@ import java.util.List;
*
* @author LevelX2
*/
public class MageObjectAttribute implements Serializable {
public class MageObjectAttribute implements Serializable, Copyable<MageObjectAttribute> {
protected final ObjectColor color;
protected final SubTypes subtype;

View file

@ -144,13 +144,8 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.maxBlocks = permanent.maxBlocks;
this.deathtouched = permanent.deathtouched;
this.markedLifelink = permanent.markedLifelink;
for (Map.Entry<String, List<UUID>> entry : permanent.connectedCards.entrySet()) {
this.connectedCards.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
if (permanent.dealtDamageByThisTurn != null) {
dealtDamageByThisTurn = new HashSet<>(permanent.dealtDamageByThisTurn);
}
this.connectedCards = CardUtil.deepCopyObject(permanent.connectedCards);
this.dealtDamageByThisTurn = CardUtil.deepCopyObject(permanent.dealtDamageByThisTurn);
if (permanent.markedDamage != null) {
markedDamage = new ArrayList<>();
for (MarkedDamageInfo mdi : permanent.markedDamage) {

View file

@ -1,9 +1,6 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.MageObject;
import mage.MageObjectImpl;
import mage.ObjectColor;
import mage.*;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.effects.Effect;
@ -316,6 +313,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
// tokens zcc must simulate card's zcc to keep copied card/spell settings
// (example: etb's kicker ability of copied creature spell, see tests with Deathforge Shaman)
newPermanent.updateZoneChangeCounter(game, emptyEvent);
if (source != null) {
MageObjectReference mor = new MageObjectReference(newPermanent.getId(),newPermanent.getZoneChangeCounter(game)-1,game);
game.storePermanentCostsTags(mor, source);
}
}
// check ETB effects

View file

@ -1,9 +1,6 @@
package mage.game.stack;
import mage.MageInt;
import mage.MageObject;
import mage.Mana;
import mage.ObjectColor;
import mage.*;
import mage.abilities.*;
import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.costs.mana.ManaCost;
@ -336,6 +333,8 @@ public class Spell extends StackObjectImpl implements Card {
}
} else {
permId = card.getId();
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
if (flag) {
@ -374,6 +373,8 @@ public class Spell extends StackObjectImpl implements Card {
}
// Aura has no legal target and its a bestow enchantment -> Add it to battlefield as creature
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
if (controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null)) {
Permanent permanent = game.getPermanent(card.getId());
if (permanent instanceof PermanentCard) {
@ -397,6 +398,8 @@ public class Spell extends StackObjectImpl implements Card {
token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false);
return true;
} else {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
}

View file

@ -33,10 +33,7 @@ import mage.util.SubTypes;
import mage.util.functions.StackObjectCopyApplier;
import mage.watchers.Watcher;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
@ -402,7 +399,18 @@ public class StackAbility extends StackObjectImpl implements Ability {
public void addManaCostsToPay(ManaCost manaCost) {
// Do nothing
}
@Override
public Map<String, Object> getCostsTagMap() {
return ability.getCostsTagMap();
}
@Override
public void setCostsTag(String tag, Object value){
ability.setCostsTag(tag, value);
}
@Override
public Object getCostsTagOrDefault(String tag, Object defaultValue){
return ability.getCostsTagOrDefault(tag, defaultValue);
}
@Override
public AbilityType getAbilityType() {
return ability.getAbilityType();

View file

@ -8,6 +8,7 @@ import mage.constants.PhaseStep;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.util.Copyable;
/**
* Game's step
@ -17,7 +18,7 @@ import mage.game.events.GameEvent.EventType;
*
* @author BetaSteward_at_googlemail.com
*/
public abstract class Step implements Serializable {
public abstract class Step implements Serializable, Copyable<Step> {
private final PhaseStep type;
private final boolean hasPriority;

View file

@ -1,10 +1,7 @@
package mage.util;
import com.google.common.collect.ImmutableList;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.Mana;
import mage.*;
import mage.abilities.*;
import mage.abilities.condition.Condition;
import mage.abilities.costs.Cost;
@ -42,9 +39,11 @@ import mage.game.permanent.token.Token;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.players.PlayerList;
import mage.target.Target;
import mage.target.TargetCard;
import mage.target.targetpointer.FixedTarget;
import mage.watchers.Watcher;
import org.apache.log4j.Logger;
import java.io.UnsupportedEncodingException;
@ -1648,6 +1647,75 @@ public final class CardUtil {
}
return zcc;
}
/**
* Create a MageObjectReference of the ability's source
* Subtract 1 zcc if not on the stack, referencing when it was on the stack if it's a resolved permanent.
* works in any moment (even before source ability activated)
*
* @param game
* @param ability
* @return MageObjectReference to the ability's source stack moment
*/
public static MageObjectReference getSourceStackMomentReference(Game game, Ability ability) {
// Squad/Kicker activates in STACK zone so all zcc must be from "stack moment"
// Use cases:
// * resolving spell have same zcc (example: check kicker status in sorcery/instant);
// * copied spell have same zcc as source spell (see Spell.copySpell and zcc sync);
// * creature/token from resolved spell have +1 zcc after moved to battlefield (example: check kicker status in ETB triggers/effects);
// find object info from the source ability (it can be a permanent or a spell on stack, on the moment of trigger/resolve)
MageObject sourceObject = ability.getSourceObject(game);
Zone sourceObjectZone = game.getState().getZone(sourceObject.getId());
int zcc = CardUtil.getActualSourceObjectZoneChangeCounter(game, ability);
// find "stack moment" zcc:
// * permanent cards enters from STACK to BATTLEFIELD (+1 zcc)
// * permanent tokens enters from OUTSIDE to BATTLEFIELD (+1 zcc, see prepare code in TokenImpl.putOntoBattlefieldHelper)
// * spells and copied spells resolves on STACK (zcc not changes)
if (sourceObjectZone != Zone.STACK) {
--zcc;
}
return new MageObjectReference(ability.getSourceId(), zcc, game);
}
//Use the two other functions below to access the tags, this is just the shared logic for them
private static Map<String, Object> getCostsTags(Game game, Ability source) {
Map<String, Object> costTags;
costTags = source.getCostsTagMap();
if (costTags == null && source.getSourcePermanentOrLKI(game) != null) {
costTags = game.getPermanentCostsTags().get(CardUtil.getSourceStackMomentReference(game, source));
}
return costTags;
}
/**
* Check if a specific tag exists in the cost tags of either the source ability, or the permanent source of the ability.
* Works in any moment (even before source ability activated)
*
* @param game
* @param source
* @param tag The tag's string identifier to look up
* @return if the tag was found
*/
public static boolean checkSourceCostsTagExists(Game game, Ability source, String tag) {
Map<String, Object> costTags = getCostsTags(game, source);
return costTags != null && costTags.containsKey(tag);
}
/**
* Find a specific tag in the cost tags of either the source ability, or the permanent source of the ability.
* Works in any moment (even before source ability activated)
*
* @param game
* @param source
* @param tag The tag's string identifier to look up
* @param defaultValue A default value to return if the tag is not found
* @return The object stored by the tag if found, the default if not
*/
public static Object getSourceCostsTag(Game game, Ability source, String tag, Object defaultValue){
Map<String, Object> costTags = getCostsTags(game, source);
if (costTags != null) {
return costTags.getOrDefault(tag, defaultValue);
}
return defaultValue;
}
public static String addCostVerb(String text) {
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
@ -1656,6 +1724,117 @@ public final class CardUtil {
return "pay " + text;
}
private static boolean isImmutableObject(Object o){
return o == null
|| o instanceof Number || o instanceof Boolean || o instanceof String
|| o instanceof MageObjectReference || o instanceof UUID
|| o instanceof Enum;
}
public static <T> T deepCopyObject(T value){
if (isImmutableObject(value)) {
return value;
} else if (value instanceof Copyable) {
return (T) ((Copyable<T>) value).copy();
} else if (value instanceof Watcher) {
return (T) ((Watcher) value).copy();
} else if (value instanceof Ability) {
return (T) ((Ability) value).copy();
} else if (value instanceof PlayerList) {
return (T) ((PlayerList) value).copy();
} else if (value instanceof EnumSet) {
return (T) ((EnumSet) value).clone();
} else if (value instanceof EnumMap) {
return (T) deepCopyEnumMap((EnumMap) value);
} else if (value instanceof LinkedHashSet) {
return (T) deepCopyLinkedHashSet((LinkedHashSet) value);
} else if (value instanceof LinkedHashMap) {
return (T) deepCopyLinkedHashMap((LinkedHashMap) value);
} else if (value instanceof TreeSet) {
return (T) deepCopyTreeSet((TreeSet) value);
} else if (value instanceof HashSet) {
return (T) deepCopyHashSet((HashSet) value);
} else if (value instanceof HashMap) {
return (T) deepCopyHashMap((HashMap) value);
} else if (value instanceof List) {
return (T) deepCopyList((List) value);
} else if (value instanceof AbstractMap.SimpleImmutableEntry){ //Used by Leonin Arbiter, Vessel Of The All Consuming Wanderer as a generic Pair class
AbstractMap.SimpleImmutableEntry entryValue = (AbstractMap.SimpleImmutableEntry) value;
return (T) new AbstractMap.SimpleImmutableEntry(deepCopyObject(entryValue.getKey()),deepCopyObject(entryValue.getValue()));
} else {
throw new IllegalStateException("Unhandled object " + value.getClass().getSimpleName() + " during deep copy, must add explicit handling of all Object types");
}
}
private static <T extends Comparable<T>> TreeSet<T> deepCopyTreeSet(TreeSet<T> original) {
if (original.getClass() != TreeSet.class) {
throw new IllegalStateException("Unhandled TreeSet type " + original.getClass().getSimpleName() + " in deep copy");
}
TreeSet<T> newSet = new TreeSet<>();
for (T value : original){
newSet.add((T) deepCopyObject(value));
}
return newSet;
}
private static <T> HashSet<T> deepCopyHashSet(Set<T> original) {
if (original.getClass() != HashSet.class) {
throw new IllegalStateException("Unhandled HashSet type " + original.getClass().getSimpleName() + " in deep copy");
}
HashSet<T> newSet = new HashSet<>(original.size());
for (T value : original){
newSet.add((T) deepCopyObject(value));
}
return newSet;
}
private static <T> LinkedHashSet<T> deepCopyLinkedHashSet(LinkedHashSet<T> original) {
if (original.getClass() != LinkedHashSet.class) {
throw new IllegalStateException("Unhandled LinkedHashSet type " + original.getClass().getSimpleName() + " in deep copy");
}
LinkedHashSet<T> newSet = new LinkedHashSet<>(original.size());
for (T value : original){
newSet.add((T) deepCopyObject(value));
}
return newSet;
}
private static <T> List<T> deepCopyList(List<T> original) { //always returns an ArrayList
if (original.getClass() != ArrayList.class) {
throw new IllegalStateException("Unhandled List type " + original.getClass().getSimpleName() + " in deep copy");
}
ArrayList<T> newList = new ArrayList<>(original.size());
for (T value : original){
newList.add((T) deepCopyObject(value));
}
return newList;
}
private static <K, V> HashMap<K, V> deepCopyHashMap(Map<K, V> original) {
if (original.getClass() != HashMap.class) {
throw new IllegalStateException("Unhandled HashMap type " + original.getClass().getSimpleName() + " in deep copy");
}
HashMap<K, V> newMap = new HashMap<>(original.size());
for (Map.Entry<K, V> entry : original.entrySet()) {
newMap.put((K) deepCopyObject(entry.getKey()), (V) deepCopyObject(entry.getValue()));
}
return newMap;
}
private static <K, V> LinkedHashMap<K, V> deepCopyLinkedHashMap(Map<K, V> original) {
if (original.getClass() != LinkedHashMap.class) {
throw new IllegalStateException("Unhandled LinkedHashMap type " + original.getClass().getSimpleName() + " in deep copy");
}
LinkedHashMap<K, V> newMap = new LinkedHashMap<>(original.size());
for (Map.Entry<K, V> entry : original.entrySet()) {
newMap.put((K) deepCopyObject(entry.getKey()), (V) deepCopyObject(entry.getValue()));
}
return newMap;
}
private static <K extends Enum<K>, V> EnumMap<K, V> deepCopyEnumMap(Map<K, V> original) {
if (original.getClass() != EnumMap.class) {
throw new IllegalStateException("Unhandled EnumMap type " + original.getClass().getSimpleName() + " in deep copy");
}
EnumMap<K, V> newMap = new EnumMap<>(original);
for (Map.Entry<K, V> entry : newMap.entrySet()) {
entry.setValue((V) deepCopyObject(entry.getValue()));
}
return newMap;
}
/**
* Collect all possible object's parts (example: all sides in mdf/split cards)
* <p>

View file

@ -1,11 +1,9 @@
package mage.watchers;
import mage.cards.Cards;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.PlayerList;
import mage.util.Copyable;
import mage.util.CardUtil;
import org.apache.log4j.Logger;
import java.io.Serializable;
@ -114,96 +112,7 @@ public abstract class Watcher implements Serializable {
for (Field field : allFields) {
if (!Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
if (field.getType() == Set.class) {
// Set<UUID, xxx>
((Set) field.get(watcher)).clear();
((Set) field.get(watcher)).addAll((Set) field.get(this));
} else if (field.getType() == Map.class || field.getType() == HashMap.class) {
// Map<UUID, xxx>
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type valueType = parameterizedType.getActualTypeArguments()[1];
if (valueType.getTypeName().contains("SortedSet")) {
// Map<UUID, SortedSet<Object>>
Map<Object, Set<Object>> source = (Map<Object, Set<Object>>) field.get(this);
Map<Object, Set<Object>> target = (Map<Object, Set<Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Set<Object>> e : source.entrySet()) {
Set<Object> set = new TreeSet<>();
set.addAll(e.getValue());
target.put(e.getKey(), set);
}
} else if (valueType.getTypeName().contains("Set")) {
// Map<UUID, Set<Object>>
Map<Object, Set<Object>> source = (Map<Object, Set<Object>>) field.get(this);
Map<Object, Set<Object>> target = (Map<Object, Set<Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Set<Object>> e : source.entrySet()) {
Set<Object> set = new HashSet<>();
set.addAll(e.getValue());
target.put(e.getKey(), set);
}
} else if (valueType.getTypeName().contains("PlayerList")) {
// Map<UUID, PlayerList>
Map<Object, PlayerList> source = (Map<Object, PlayerList>) field.get(this);
Map<Object, PlayerList> target = (Map<Object, PlayerList>) field.get(watcher);
target.clear();
for (Map.Entry<Object, PlayerList> e : source.entrySet()) {
PlayerList list = e.getValue().copy();
target.put(e.getKey(), list);
}
} else if (valueType.getTypeName().endsWith("Cards")) {
// Map<UUID, Cards>
Map<Object, Cards> source = (Map<Object, Cards>) field.get(this);
Map<Object, Cards> target = (Map<Object, Cards>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Cards> e : source.entrySet()) {
Cards list = e.getValue().copy();
target.put(e.getKey(), list);
}
} else if (valueType instanceof Class && Arrays.stream(((Class) valueType).getInterfaces()).anyMatch(c -> c.equals(Copyable.class))) {
// Map<UUID, Copyable>
Map<Object, Copyable> source = (Map<Object, Copyable>) field.get(this);
Map<Object, Copyable> target = (Map<Object, Copyable>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Copyable> e : source.entrySet()) {
Copyable object = (Copyable) e.getValue().copy();
target.put(e.getKey(), object);
}
} else if (valueType.getTypeName().contains("List")) {
// Map<UUID, List<Object>>
Map<Object, List<Object>> source = (Map<Object, List<Object>>) field.get(this);
Map<Object, List<Object>> target = (Map<Object, List<Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, List<Object>> e : source.entrySet()) {
List<Object> list = new ArrayList<>();
list.addAll(e.getValue());
target.put(e.getKey(), list);
}
} else if (valueType.getTypeName().contains("Map")) {
// Map<UUID, Map<UUID, Object>>
Map<Object, Map<Object, Object>> source = (Map<Object, Map<Object, Object>>) field.get(this);
Map<Object, Map<Object, Object>> target = (Map<Object, Map<Object, Object>>) field.get(watcher);
target.clear();
for (Map.Entry<Object, Map<Object, Object>> e : source.entrySet()) {
Map<Object, Object> map = new HashMap<>();
map.putAll(e.getValue());
target.put(e.getKey(), map);
}
} else {
// Map<UUID, Object>
// TODO: add additional tests to find unsupported watcher data
((Map) field.get(watcher)).putAll((Map) field.get(this));
}
} else if (field.getType() == List.class) {
// List<Object>
((List) field.get(watcher)).clear();
((List) field.get(watcher)).addAll((List) field.get(this));
} else {
// Object
field.set(watcher, field.get(this));
}
field.set(watcher, CardUtil.deepCopyObject(field.get(this)));
}
}
return watcher;

View file

@ -25,8 +25,6 @@ import java.util.UUID;
public class ManaSpentToCastWatcher extends Watcher {
private final Map<UUID, Mana> manaMap = new HashMap<>();
private final Map<UUID, Integer> xValueMap = new HashMap<>();
private final Map<UUID, Integer> xValueMapLong = new HashMap<>(); // do not reset, keep until game end
public ManaSpentToCastWatcher() {
super(WatcherScope.GAME);
@ -40,15 +38,11 @@ public class ManaSpentToCastWatcher extends Watcher {
Spell spell = (Spell) game.getObject(event.getTargetId());
if (spell != null) {
manaMap.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getUsedManaToPay());
xValueMap.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getX());
xValueMapLong.put(spell.getSourceId(), spell.getSpellAbility().getManaCostsToPay().getX());
}
return;
case ZONE_CHANGE:
if (((ZoneChangeEvent) event).getFromZone() == Zone.BATTLEFIELD) {
manaMap.remove(event.getTargetId());
xValueMap.remove(event.getTargetId());
xValueMapLong.remove(event.getTargetId());
}
}
}
@ -57,29 +51,9 @@ public class ManaSpentToCastWatcher extends Watcher {
return manaMap.getOrDefault(sourceId, null);
}
/**
* Return X value for casted spell or permanents
*
* @param source
* @param useLongSource - use X value that keeps until end of game (for info only)
* @return
*/
public int getLastXValue(Ability source, boolean useLongSource) {
Map<UUID, Integer> xSource = useLongSource ? this.xValueMapLong : this.xValueMap;
if (xSource.containsKey(source.getSourceId())) {
// cast normal way
return xSource.get(source.getSourceId());
} else {
// put to battlefield without cast (example: copied spell must keep announced X)
return source.getManaCostsToPay().getX();
}
}
@Override
public void reset() {
super.reset();
manaMap.clear();
xValueMap.clear();
// xValueMapLong.clear(); // must keep until game end, so don't clear between turns
}
}