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

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