From 2565676740b005d4daebd5ce5682b9a36379453d Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 00:47:54 -0400 Subject: [PATCH 1/9] Put some XCount into a cache --- forge-game/src/main/java/forge/game/Game.java | 4 + .../java/forge/game/ability/AbilityUtils.java | 81 +++++++++++++++++-- .../main/java/forge/game/player/Player.java | 16 ++++ .../src/main/java/forge/game/zone/Zone.java | 1 + 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index c9f8171f7c2..30959eed5e4 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -1378,4 +1378,8 @@ public int getAITimeout() { public boolean canUseTimeout() { return AI_CAN_USE_TIMEOUT; } + + public void resetCache() { + getPlayers().forEach(p -> p.resetCache()); + } } diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 2f8d310fcc7..190a4bbb30e 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1597,6 +1597,13 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } + if (player != null) { + Integer cached = player.getFromCache(s); + if (cached != null) { + return cached; + } + } + // accept straight numbers if (l[0].startsWith("Number$")) { final String number = l[0].substring(7); @@ -1610,7 +1617,14 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { if (l[0].startsWith("SVar$")) { String n = l[0].substring(5); String v = ctb == null ? c.getSVar(n) : ctb.getSVar(n); - return doXMath(xCount(c, v, ctb), expr, c, ctb); + + int value = doXMath(xCount(c, v, ctb), expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } final String[] sq; @@ -1947,7 +1961,14 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } colorOccurrences += player.getDevotionMod(); - return doXMath(colorOccurrences, expr, c, ctb); + + int value = doXMath(colorOccurrences, expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } } // end ctb != null @@ -2586,7 +2607,14 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { count++; } } - return doXMath(count, expr, c, ctb); + + int value = doXMath(count, expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } if (sq[0].contains("Party")) { @@ -2718,7 +2746,13 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - return doXMath(colorOcurrencices, expr, c, ctb); + int value = doXMath(colorOcurrencices, expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } if (l[0].contains("ExactManaCost")) { @@ -2737,7 +2771,13 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } manaCost.remove(ManaCost.NO_COST.getShortString()); - return doXMath(manaCost.size(), expr, c, ctb); + int value = doXMath(manaCost.size(), expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } if (sq[0].equals("StormCount")) { @@ -2866,6 +2906,11 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { max = entry.getValue(); } } + + if (player != null) { + player.putInCache(s, max); + } + return max; } @@ -2889,7 +2934,14 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { .filter(CardPredicates.restriction(restriction, player, c, ctb)) .map(Card::getNetPower) .distinct().count(); - return doXMath(uniquePowers, expr, c, ctb); + + int value = doXMath(uniquePowers, expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } if (sq[0].startsWith("DifferentCounterKinds_")) { final Set kinds = Sets.newHashSet(); @@ -2898,7 +2950,14 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { for (final Card card : list) { kinds.addAll(card.getCounters().keySet()); } - return doXMath(kinds.size(), expr, c, ctb); + + int value = doXMath(kinds.size(), expr, c, ctb); + + if (player != null) { + player.putInCache(s, value); + } + + return value; } // Complex counting methods @@ -2912,7 +2971,13 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { num = Iterables.size(someCards); } - return doXMath(num, expr, c, ctb); + int numValue = doXMath(num, expr, c, ctb); + + if (player != null) { + player.putInCache(s, numValue); + } + + return numValue; } public static final void applyManaColorConversion(ManaConversionMatrix matrix, String conversion) { diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 60525c5a051..3cc8ad104ea 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -215,6 +215,9 @@ public class Player extends GameEntity implements Comparable { private final AchievementTracker achievementTracker = new AchievementTracker(); private final PlayerView view; + // Query cache to avoid repeated expensive queries + private final Map queryCache = new HashMap<>(); + public Player(String name0, Game game0, final int id0) { super(id0); @@ -433,6 +436,7 @@ else if (newLife > life) { else { // life == newLife change = false; } + return change; } @@ -4088,4 +4092,16 @@ public void triggerElementalBend(TriggerType type) { public boolean hasAllElementBend() { return elementalBendThisTurn.size() >= 4; } + + public Integer getFromCache(String queryKey) { + return queryCache.get(queryKey); + } + + public void putInCache(String queryKey, int value) { + queryCache.put(queryKey, value); + } + + public void resetCache() { + queryCache.clear(); + } } diff --git a/forge-game/src/main/java/forge/game/zone/Zone.java b/forge-game/src/main/java/forge/game/zone/Zone.java index 5d57174a4db..8f6a21dd026 100644 --- a/forge-game/src/main/java/forge/game/zone/Zone.java +++ b/forge-game/src/main/java/forge/game/zone/Zone.java @@ -68,6 +68,7 @@ public Zone(final ZoneType zone0, Game game0) { } protected void onChanged() { + game.resetCache(); } public Player getPlayer() { // generic zones like stack have no player associated From bf1ce2ff84de281865f7204d11ee641c779ade18 Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 13:02:33 -0400 Subject: [PATCH 2/9] Update Realmbreaker, the Invasion Tree (Warning because SVar starts with X) --- .../res/cardsfolder/r/realmbreaker_the_invasion_tree.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/r/realmbreaker_the_invasion_tree.txt b/forge-gui/res/cardsfolder/r/realmbreaker_the_invasion_tree.txt index 34e81de0877..74d6b48eba1 100644 --- a/forge-gui/res/cardsfolder/r/realmbreaker_the_invasion_tree.txt +++ b/forge-gui/res/cardsfolder/r/realmbreaker_the_invasion_tree.txt @@ -6,9 +6,9 @@ SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield SVar:DBAnimate:DB$ Animate | Replacements$ ReplaceLeaves | Defined$ Remembered | Duration$ Permanent | SubAbility$ DBCleanup | StackDescription$ None SVar:ReplaceLeaves:Event$ Moved | ActiveZones$ Battlefield | Origin$ Battlefield | ValidCard$ Card.Self | ReplaceWith$ Exile | Description$ If this land would leave the battlefield, exile it instead of putting it anywhere else. SVar:Exile:DB$ ChangeZone | Origin$ Battlefield | Destination$ Exile | Defined$ ReplacedCard -A:AB$ ChangeZone | Cost$ 10 T Sac<1/CARDNAME> | Origin$ Library | Destination$ Battlefield | ChangeType$ Praetor | ChangeNum$ XFetch | StackDescription$ {p:You} searches their library for any number of Praetor cards, puts them onto the battlefield, then shuffles. | SpellDescription$ Search your library for any number of Praetor cards, put them onto the battlefield, then shuffle. +A:AB$ ChangeZone | Cost$ 10 T Sac<1/CARDNAME> | Origin$ Library | Destination$ Battlefield | ChangeType$ Praetor | ChangeNum$ FetchCount | StackDescription$ {p:You} searches their library for any number of Praetor cards, puts them onto the battlefield, then shuffles. | SpellDescription$ Search your library for any number of Praetor cards, put them onto the battlefield, then shuffle. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:XFetch:Count$ValidLibrary Praetor.YouCtrl +SVar:FetchCount:Count$ValidLibrary Praetor.YouCtrl DeckHas:Ability$Mill|Sacrifice DeckHints:Type$Praetor Oracle:{2}, {T}: Target opponent mills three cards. Put a land card from their graveyard onto the battlefield tapped under your control. It gains "If this land would leave the battlefield, exile it instead of putting it anywhere else."\n{10}, {T}, Sacrifice Realmbreaker, the Invasion Tree: Search your library for any number of Praetor cards, put them onto the battlefield, then shuffle. From 476e092dc7772ef9c4b9ec110777c80e59c651de Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 13:03:05 -0400 Subject: [PATCH 3/9] Ensure player zones clear cache --- forge-game/src/main/java/forge/game/zone/PlayerZone.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forge-game/src/main/java/forge/game/zone/PlayerZone.java b/forge-game/src/main/java/forge/game/zone/PlayerZone.java index 3c3dccc02d0..312c0a95531 100644 --- a/forge-game/src/main/java/forge/game/zone/PlayerZone.java +++ b/forge-game/src/main/java/forge/game/zone/PlayerZone.java @@ -96,6 +96,8 @@ public PlayerZone(final ZoneType zone, final Player inPlayer) { @Override protected void onChanged() { + super.onChanged(); + if (getZoneType() == ZoneType.Hand && player.getController().isOrderedZone()) { sort(); } From bc2962d35326292f07d0a2e59d7facac76d4cdf5 Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 13:03:21 -0400 Subject: [PATCH 4/9] Cleanup --- .../java/forge/game/ability/AbilityUtils.java | 91 ++++++------------- 1 file changed, 27 insertions(+), 64 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 190a4bbb30e..30baf54241e 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -428,7 +428,7 @@ else if (ability != null) { svarval = ability.getSVar(amount); } if (StringUtils.isBlank(svarval)) { - if ((ability != null) && (ability instanceof SpellAbility) && !(ability instanceof SpellPermanent)) { + if ((ability instanceof SpellAbility) && !(ability instanceof SpellPermanent)) { System.err.printf("SVar '%s' not found in ability, fallback to Card (%s). Ability is (%s)%n", amount, card.getName(), ability); } svarval = card.getSVar(amount); @@ -1598,9 +1598,9 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } if (player != null) { - Integer cached = player.getFromCache(s); - if (cached != null) { - return cached; + Integer cachedValue = player.getFromCache(s); + if (cachedValue != null) { + return cachedValue; } } @@ -1618,13 +1618,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { String n = l[0].substring(5); String v = ctb == null ? c.getSVar(n) : ctb.getSVar(n); - int value = doXMath(xCount(c, v, ctb), expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(xCount(c, v, ctb), expr, c, ctb)); } final String[] sq; @@ -1962,13 +1956,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } colorOccurrences += player.getDevotionMod(); - int value = doXMath(colorOccurrences, expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(colorOccurrences, expr, c, ctb)); } } // end ctb != null @@ -2608,13 +2596,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - int value = doXMath(count, expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(count, expr, c, ctb)); } if (sq[0].contains("Party")) { @@ -2746,13 +2728,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - int value = doXMath(colorOcurrencices, expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(colorOcurrencices, expr, c, ctb)); } if (l[0].contains("ExactManaCost")) { @@ -2771,13 +2747,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } manaCost.remove(ManaCost.NO_COST.getShortString()); - int value = doXMath(manaCost.size(), expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(manaCost.size(), expr, c, ctb)); } if (sq[0].equals("StormCount")) { @@ -2907,11 +2877,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - if (player != null) { - player.putInCache(s, max); - } - - return max; + return computeAndCache(player, s, max); } if (sq[0].startsWith("MostProminentCreatureType")) { @@ -2935,13 +2901,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { .map(Card::getNetPower) .distinct().count(); - int value = doXMath(uniquePowers, expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(uniquePowers, expr, c, ctb)); } if (sq[0].startsWith("DifferentCounterKinds_")) { final Set kinds = Sets.newHashSet(); @@ -2951,13 +2911,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { kinds.addAll(card.getCounters().keySet()); } - int value = doXMath(kinds.size(), expr, c, ctb); - - if (player != null) { - player.putInCache(s, value); - } - - return value; + return computeAndCache(player, s, doXMath(kinds.size(), expr, c, ctb)); } // Complex counting methods @@ -2971,13 +2925,18 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { num = Iterables.size(someCards); } - int numValue = doXMath(num, expr, c, ctb); + return computeAndCache(player, s, doXMath(num, expr, c, ctb)); + } - if (player != null) { - player.putInCache(s, numValue); + /** + Caches the computed value if possible and returns it. + */ + private static Integer computeAndCache(Player p, String key, int value) { + if (p != null) { + p.putInCache(key, value); } - return numValue; + return value; } public static final void applyManaColorConversion(ManaConversionMatrix matrix, String conversion) { @@ -3473,11 +3432,15 @@ public static int playerXProperty(final Player player, final String s, final Car final Game game = player.getGame(); + if (player.getFromCache(s) != null) { + return player.getFromCache(s); + } + // count valid cards on the battlefield if (l[0].startsWith("Valid ")) { final String restrictions = l[0].substring(6); int num = CardLists.getValidCardCount(game.getCardsIn(ZoneType.Battlefield), restrictions, player, source, ctb); - return doXMath(num, m, source, ctb); + return computeAndCache(player, s, doXMath(num, m, source, ctb)); } // count valid cards in any specified zone/s @@ -3486,7 +3449,7 @@ public static int playerXProperty(final Player player, final String s, final Car final List vZone = ZoneType.listValueOf(lparts[0].split("Valid")[1]); String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), ""); int num = CardLists.getValidCardCount(game.getCardsIn(vZone), restrictions, player, source, ctb); - return doXMath(num, m, source, ctb); + return computeAndCache(player, s, doXMath(num, m, source, ctb)); } if (l[0].startsWith("ThisTurnEntered")) { From a4d4e5a635fdd36a3eeafdeddb534649b239abf3 Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 16:58:20 -0400 Subject: [PATCH 5/9] Cache clear + do not cache certain on keywords --- forge-game/src/main/java/forge/game/Game.java | 6 ++- .../java/forge/game/ability/AbilityUtils.java | 43 +++++++++++-------- .../java/forge/game/phase/PhaseHandler.java | 1 + .../src/main/java/forge/game/zone/Zone.java | 2 +- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 30959eed5e4..79841b7bdc7 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -825,6 +825,7 @@ public int getPosition(Player player, Player startingPlayer) { } public void onPlayerLost(Player p) { + clearPlayersCache(); //set for Avatar p.setHasLost(true); // Rule 800.4 Losing a Multiplayer game @@ -1379,7 +1380,10 @@ public boolean canUseTimeout() { return AI_CAN_USE_TIMEOUT; } - public void resetCache() { + /** + * Reset players' cache. + */ + public void clearPlayersCache() { getPlayers().forEach(p -> p.resetCache()); } } diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 30baf54241e..eedfb08cb63 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1583,6 +1583,7 @@ public static void handleRemembering(final SpellAbility sa) { * @return a int. */ public static int xCount(Card c, final String s, final CardTraitBase ctb) { + String cacheKey = "xCount_" + s; final String s2 = applyAbilityTextChangeEffects(s, ctb); final String[] l = s2.split("/"); final String expr = CardFactoryUtil.extractOperators(s2); @@ -1598,7 +1599,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } if (player != null) { - Integer cachedValue = player.getFromCache(s); + Integer cachedValue = player.getFromCache(cacheKey); if (cachedValue != null) { return cachedValue; } @@ -1618,7 +1619,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { String n = l[0].substring(5); String v = ctb == null ? c.getSVar(n) : ctb.getSVar(n); - return computeAndCache(player, s, doXMath(xCount(c, v, ctb), expr, c, ctb)); + return doXMath(xCount(c, v, ctb), expr, c, ctb); } final String[] sq; @@ -1956,7 +1957,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } colorOccurrences += player.getDevotionMod(); - return computeAndCache(player, s, doXMath(colorOccurrences, expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(colorOccurrences, expr, c, ctb)); } } // end ctb != null @@ -2596,7 +2597,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - return computeAndCache(player, s, doXMath(count, expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(count, expr, c, ctb)); } if (sq[0].contains("Party")) { @@ -2728,7 +2729,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - return computeAndCache(player, s, doXMath(colorOcurrencices, expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(colorOcurrencices, expr, c, ctb)); } if (l[0].contains("ExactManaCost")) { @@ -2747,7 +2748,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } manaCost.remove(ManaCost.NO_COST.getShortString()); - return computeAndCache(player, s, doXMath(manaCost.size(), expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(manaCost.size(), expr, c, ctb)); } if (sq[0].equals("StormCount")) { @@ -2877,7 +2878,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { } } - return computeAndCache(player, s, max); + return computeAndCache(player, cacheKey, max); } if (sq[0].startsWith("MostProminentCreatureType")) { @@ -2901,7 +2902,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { .map(Card::getNetPower) .distinct().count(); - return computeAndCache(player, s, doXMath(uniquePowers, expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(uniquePowers, expr, c, ctb)); } if (sq[0].startsWith("DifferentCounterKinds_")) { final Set kinds = Sets.newHashSet(); @@ -2911,7 +2912,7 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { kinds.addAll(card.getCounters().keySet()); } - return computeAndCache(player, s, doXMath(kinds.size(), expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(kinds.size(), expr, c, ctb)); } // Complex counting methods @@ -2925,17 +2926,25 @@ public static int xCount(Card c, final String s, final CardTraitBase ctb) { num = Iterables.size(someCards); } - return computeAndCache(player, s, doXMath(num, expr, c, ctb)); + return computeAndCache(player, cacheKey, doXMath(num, expr, c, ctb)); } /** Caches the computed value if possible and returns it. */ private static Integer computeAndCache(Player p, String key, int value) { - if (p != null) { - p.putInCache(key, value); + if (p == null){ + return value; } + if (key.contains("Remembered") || + key.contains("Triggered") || + key.contains("Chosen")) { + return value; + } + + p.putInCache(key, value); + return value; } @@ -3426,21 +3435,21 @@ public static int playerXCount(final List players, final String s, final } public static int playerXProperty(final Player player, final String s, final Card source, CardTraitBase ctb) { - + final String cacheKey = "playerXProperty_" + s; final String[] l = s.split("/"); final String m = CardFactoryUtil.extractOperators(s); final Game game = player.getGame(); - if (player.getFromCache(s) != null) { - return player.getFromCache(s); + if (player.getFromCache(cacheKey) != null) { + return player.getFromCache(cacheKey); } // count valid cards on the battlefield if (l[0].startsWith("Valid ")) { final String restrictions = l[0].substring(6); int num = CardLists.getValidCardCount(game.getCardsIn(ZoneType.Battlefield), restrictions, player, source, ctb); - return computeAndCache(player, s, doXMath(num, m, source, ctb)); + return computeAndCache(player, cacheKey, doXMath(num, m, source, ctb)); } // count valid cards in any specified zone/s @@ -3449,7 +3458,7 @@ public static int playerXProperty(final Player player, final String s, final Car final List vZone = ZoneType.listValueOf(lparts[0].split("Valid")[1]); String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), ""); int num = CardLists.getValidCardCount(game.getCardsIn(vZone), restrictions, player, source, ctb); - return computeAndCache(player, s, doXMath(num, m, source, ctb)); + return computeAndCache(player, cacheKey, doXMath(num, m, source, ctb)); } if (l[0].startsWith("ThisTurnEntered")) { diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 035ee178385..f1e7f10c015 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -175,6 +175,7 @@ private void advanceToNextPhase() { if (turnEnded) { turn++; extraPhases.clear(); + game.clearPlayersCache(); game.updateTurnForView(); game.fireEvent(new GameEventTurnBegan(playerTurn, turn)); diff --git a/forge-game/src/main/java/forge/game/zone/Zone.java b/forge-game/src/main/java/forge/game/zone/Zone.java index 8f6a21dd026..b339334d852 100644 --- a/forge-game/src/main/java/forge/game/zone/Zone.java +++ b/forge-game/src/main/java/forge/game/zone/Zone.java @@ -68,7 +68,7 @@ public Zone(final ZoneType zone0, Game game0) { } protected void onChanged() { - game.resetCache(); + game.clearPlayersCache(); } public Player getPlayer() { // generic zones like stack have no player associated From 2662a04dd1c66b7c4eadc1356ef2339ab1548996 Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 16:58:30 -0400 Subject: [PATCH 6/9] Cleanup --- forge-game/src/main/java/forge/game/ability/AbilityFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index f0c89aa25d2..d274c4ded30 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -459,7 +459,7 @@ private static AbilitySub getSubAbility(CardState state, String sSub, final IHas if (sVarHolder.hasSVar(sSub)) { return (AbilitySub) AbilityFactory.getAbility(state, sSub, sVarHolder); } - System.out.println("SubAbility '"+ sSub +"' not found for: " + state.getName()); + System.err.println("SubAbility '"+ sSub +"' not found for: " + state.getName()); return null; } From 7de9b481e2ba14afefe7791455ce115376b4d705 Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 16:58:40 -0400 Subject: [PATCH 7/9] Update The Death of Gwen Stacy --- forge-gui/res/cardsfolder/t/the_death_of_gwen_stacy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/t/the_death_of_gwen_stacy.txt b/forge-gui/res/cardsfolder/t/the_death_of_gwen_stacy.txt index 1a0f5e8bd23..cb60a96de5c 100644 --- a/forge-gui/res/cardsfolder/t/the_death_of_gwen_stacy.txt +++ b/forge-gui/res/cardsfolder/t/the_death_of_gwen_stacy.txt @@ -10,7 +10,7 @@ SVar:DBDiscard:DB$ Discard | Defined$ Player.NotedForDiscard | Mode$ TgtChoose | SVar:DBLoseLife:DB$ LoseLife | Defined$ NonRememberedOwner | LifeAmount$ 3 | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | SubAbility$ DBClearNotes SVar:DBClearNotes:DB$ Pump | Defined$ Player | ClearNotedCardsFor$ Discard -SVar:DBExile:DB$ ChangeZoneAll | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Player | TgtPrompt$ Select any number of target players | TargetMin$ 0 | TargetMax$ MaxTgt | ChangeType$ Card | SubAbility$ DBDraw | StackDescription$ Exile graveyards ({p:Targeted}). | SpellDescription$ Exile any number of target players' graveyards. +SVar:DBExile:DB$ ChangeZoneAll | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Player | TgtPrompt$ Select any number of target players | TargetMin$ 0 | TargetMax$ MaxTgt | ChangeType$ Card | StackDescription$ Exile graveyards ({p:Targeted}). | SpellDescription$ Exile any number of target players' graveyards. DeckHas:Ability$Discard SVar:MaxTgt:PlayerCountPlayers$Amount Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — Destroy target creature.\nII — Each player may discard a card. Each player who doesn't loses 3 life.\nIII — Exile any number of target players' graveyards. From a2544c625b16d01e087bd045a27320b884d5c0ca Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 16:59:41 -0400 Subject: [PATCH 8/9] Update LTR --- .../editions/The Lord of the Rings Tales of Middle-earth.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/forge-gui/res/editions/The Lord of the Rings Tales of Middle-earth.txt b/forge-gui/res/editions/The Lord of the Rings Tales of Middle-earth.txt index 485fd9c94be..f4ac6fecfa8 100644 --- a/forge-gui/res/editions/The Lord of the Rings Tales of Middle-earth.txt +++ b/forge-gui/res/editions/The Lord of the Rings Tales of Middle-earth.txt @@ -878,3 +878,6 @@ A-246 M A-The One Ring @Veli Nyström 10 c_a_food_sac @Randy Gallegos 11 c_a_food_sac @Randy Gallegos 12 c_a_treasure_sac @Valera Lutfullina + +[other] +13 the_ring @Viko Menezes \ No newline at end of file From dd56ac3c3a4132e291d7e259eecfa5b2e11fe4a2 Mon Sep 17 00:00:00 2001 From: Eradev Date: Mon, 13 Oct 2025 23:38:24 -0400 Subject: [PATCH 9/9] Cache cantAttack CardCollectionView --- forge-game/src/main/java/forge/game/Game.java | 9 ++++---- .../java/forge/game/phase/PhaseHandler.java | 21 ++++++++++++++++- .../main/java/forge/game/player/Player.java | 2 +- .../StaticAbilityCantAttackBlock.java | 23 +++++++++++++++++-- .../src/main/java/forge/game/zone/Zone.java | 2 +- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 79841b7bdc7..5d8a197ed80 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -825,7 +825,7 @@ public int getPosition(Player player, Player startingPlayer) { } public void onPlayerLost(Player p) { - clearPlayersCache(); + clearShortLivedCaches(); //set for Avatar p.setHasLost(true); // Rule 800.4 Losing a Multiplayer game @@ -1381,9 +1381,10 @@ public boolean canUseTimeout() { } /** - * Reset players' cache. + * Reset short lived caches that should not persist between turns or phases. */ - public void clearPlayersCache() { - getPlayers().forEach(p -> p.resetCache()); + public void clearShortLivedCaches() { + getPlayers().forEach(p -> p.clearCache()); + phaseHandler.clearCache(); } } diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index f1e7f10c015..a359180a94b 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -95,6 +95,8 @@ public class PhaseHandler implements java.io.Serializable { private final transient Game game; + // Cached values for this phase + private transient Map cachedValuesMap = Maps.newHashMap(); public PhaseHandler(final Game game0) { game = game0; @@ -150,6 +152,8 @@ private void advanceToNextPhase() { boolean isTopsy = playerTurn.isPhasesReversed(); boolean turnEnded = false; + cachedValuesMap.clear(); + game.getStack().clearUndoStack(); //can't undo action from previous phase if (bRepeatCleanup) { // for when Cleanup needs to repeat itself @@ -175,7 +179,7 @@ private void advanceToNextPhase() { if (turnEnded) { turn++; extraPhases.clear(); - game.clearPlayersCache(); + game.clearShortLivedCaches(); game.updateTurnForView(); game.fireEvent(new GameEventTurnBegan(playerTurn, turn)); @@ -1325,4 +1329,19 @@ private void handleMultiplayerEffects() { } } } + + /** + * Get the cache of CardCollectionView objects used to optimize repeated calls + * @return + */ + public Map getCache() { + return cachedValuesMap; + } + + /** + * Clear the cache of CardCollectionView objects + */ + public void clearCache() { + cachedValuesMap.clear(); + } } diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 3cc8ad104ea..429fa056e5c 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -4101,7 +4101,7 @@ public void putInCache(String queryKey, int value) { queryCache.put(queryKey, value); } - public void resetCache() { + public void clearCache() { queryCache.clear(); } } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java index 239a964632b..9798062f10d 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantAttackBlock.java @@ -17,6 +17,9 @@ */ package forge.game.staticability; +import java.util.Map; +import java.util.stream.Collectors; + import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -25,13 +28,14 @@ import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; +import forge.game.card.CardCollectionView; import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.zone.ZoneType; /** - * The Class StaticAbility_CantBeCast. + * The Class StaticAbility_CantAttackBlock. */ public class StaticAbilityCantAttackBlock { @@ -42,7 +46,22 @@ public static boolean cantAttack(final Card attacker, final GameEntity defender) return true; } - for (final Card ca : attacker.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { + final String cacheKey = "cantAttack_CantAttackAbilities_" + attacker.getGame().getPhaseHandler().getPhase(); + final Map cache = attacker.getGame().getPhaseHandler().getCache(); + CardCollectionView collectionView = null; + + if (cache.containsKey(cacheKey)) { + collectionView = cache.get(cacheKey); + } else { + collectionView = attacker.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES) + .stream() + .filter(c -> c.getStaticAbilities().stream() + .anyMatch(ab -> ab.checkConditions(StaticAbilityMode.CantAttack))) + .collect(Collectors.toCollection(CardCollection::new)); + cache.put(cacheKey, collectionView); + } + + for (final Card ca : collectionView) { for (final StaticAbility stAb : ca.getStaticAbilities()) { if (!stAb.checkConditions(StaticAbilityMode.CantAttack)) { continue; diff --git a/forge-game/src/main/java/forge/game/zone/Zone.java b/forge-game/src/main/java/forge/game/zone/Zone.java index b339334d852..a7e802d66be 100644 --- a/forge-game/src/main/java/forge/game/zone/Zone.java +++ b/forge-game/src/main/java/forge/game/zone/Zone.java @@ -68,7 +68,7 @@ public Zone(final ZoneType zone0, Game game0) { } protected void onChanged() { - game.clearPlayersCache(); + game.clearShortLivedCaches(); } public Player getPlayer() { // generic zones like stack have no player associated