From 461c6c296dfca25f841628aba5a463a4701bd001 Mon Sep 17 00:00:00 2001 From: cuinhellcat Date: Mon, 27 Jul 2026 08:39:41 +0200 Subject: [PATCH] Keep melded permanents intact across a snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A melded permanent is two cards: the front half on the battlefield in its Meld state, and the back half held in PlayerZoneBattlefield's melded collection rather than in its card list. forEachCardInGame walks that collection, so both halves reach copyGameState — which then puts each of them in the card list, because it only ever looks at the zone type. Restoring therefore broke a meld in both directions. A snapshot taken while melded came back as two separate permanents, the back half loose on the battlefield. A snapshot taken before the meld left the back half in the melded collection, so it was both melded and on the battlefield, with the front half still pointing at it. Place a card that the snapshot holds as melded back into the melded collection, take it out of there when the snapshot does not, and carry meldedWith and the back-side flag along. Co-Authored-By: Claude Opus 5 --- .../main/java/forge/game/GameSnapshot.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/forge-game/src/main/java/forge/game/GameSnapshot.java b/forge-game/src/main/java/forge/game/GameSnapshot.java index 467a44f8f8f5..c19a95beee84 100644 --- a/forge-game/src/main/java/forge/game/GameSnapshot.java +++ b/forge-game/src/main/java/forge/game/GameSnapshot.java @@ -346,6 +346,10 @@ public void copyGameState(Game fromGame, Game toGame) { newAttachedTo.addAttachedCard(newCard); } } + // Melded or not, the front half has to point at what the snapshot had — the two + // halves are one permanent, and a stale link outlives the meld otherwise. + newCard.setMeldedWith(fromCard.getMeldedWith() == null ? null + : toGame.findById(fromCard.getMeldedWith().getId())); if (fromCard.getCloneOrigin() != null) { newCard.setCloneOrigin(toGame.findById(fromCard.getCloneOrigin().getId())); } @@ -377,7 +381,20 @@ private void setCardInCopiedGame(Game toGame, Player toPlayer, Card fromCard, Ca if (fromType.equals(ZoneType.Stack)) { toGame.getStackZone().add(newCard); newCard.setZone(toGame.getStackZone()); + } else if (isMelded(fromCard)) { + // The back half of a meld lives in the battlefield zone but in its own + // collection rather than the card list. Putting it in the list instead would + // leave it on the battlefield as a permanent of its own. + PlayerZoneBattlefield battlefield = (PlayerZoneBattlefield) toPlayer.getZone(ZoneType.Battlefield); + if (newCard.getZone() == null) { + newCard.setZone(battlefield); + } + battlefield.addToMelded(newCard); } else { + // It may have been melded in the state we are leaving behind. + if (toPlayer.getZone(ZoneType.Battlefield) instanceof PlayerZoneBattlefield battlefield) { + battlefield.removeFromMelded(newCard); + } toPlayer.getZone(fromType).add(newCard); newCard.setZone(toPlayer.getZone(fromType)); } @@ -391,9 +408,16 @@ private void setCardInCopiedGame(Game toGame, Player toPlayer, Card fromCard, Ca newCard.setSickness(fromCard.hasSickness()); //newCard.setForetold(fromCard.isForetold()); //newCard.setForetoldCostByEffect(fromCard.isForetoldCostByEffect()); + newCard.setBackSide(fromCard.isBackSide()); newCard.setState(fromCard.getCurrentStateName(), false); } + /** The back half of a meld: on the battlefield, but held apart from its card list. */ + private static boolean isMelded(Card c) { + return c.getZone() instanceof PlayerZoneBattlefield battlefield + && battlefield.getMeldedCards().contains(c); + } + private static SpellAbility findSAInCard(SpellAbility sa, Card c) { String saDesc = sa.getDescription(); for (SpellAbility cardSa : c.getAllSpellAbilities()) {