Skip to content

Extend original ID concept to apply to effects as well as abilities#14854

Open
matoro wants to merge 2 commits into
magefree:masterfrom
matoro:propaganda
Open

Extend original ID concept to apply to effects as well as abilities#14854
matoro wants to merge 2 commits into
magefree:masterfrom
matoro:propaganda

Conversation

@matoro
Copy link
Copy Markdown
Contributor

@matoro matoro commented May 3, 2026

The root cause of the issue here is that when a permanent with a replacement effect is copied, this is applied via a continuous CopyEffect. Normally this is not a problem, but for attack taxers, part of choosing whether the replacement effect applies or not is tapping mana sources, which triggers applyEffects() to run. The CopyEffect is then reapplied, and as part of it, it clears the copied abilities and generates new ones with new IDs from the blueprint. Thus, the loop checking whether all replacement effects have been consumed never terminates, because it sees a new replacement effect with a new ID it needs to apply.

This extends the concept of originalId, which abilities already have, to also apply to effects. It can be used in any scenarios which need an effect's ID to remain stable even when it is destroyed and recreated via a copy from a continuous effect.

Fixes #10372

Fixes #14754

@xenohedron
Copy link
Copy Markdown
Contributor

This seems like it could be a nice simple solution to those bugs, but I want to check a few things to confirm it's the right approach.

Can you reproduce #11560 before/after this change? There's a test case in #13465, not sure if could be impacted by the other recent Tyvar bugfix. Any opinion on whether deterministic id generating is still worth pursuing?

I assume #10802 approach is still needed for Whipgrass Entangler, because that has the additional complexity that the ability can be gained multiple times, but in the becoming a copy case the ability is there exactly once?

But what about this example: Make [[Spark Double]] a copy of [[Sivitri, Dragon Master]] with [[Oath of Teferi]] to activate twice, does it work as expected?

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

Spark Double - (Gatherer) (Scryfall) (EDHREC)

{3}{U}
Creature — Illusion
0/0
You may have this creature enter as a copy of a creature or planeswalker you control, except it enters with an additional +1/+1 counter on it if it's a creature, it enters with an additional loyalty counter on it if it's a planeswalker, and it isn't legendary.

Sivitri, Dragon Master - (Gatherer) (Scryfall) (EDHREC)

{2}{U}{B}
Legendary Planeswalker — Sivitri
4
+1: Until your next turn, creatures can't attack you or planeswalkers you control unless their controller pays 2 life for each of those creatures.
−3: Search your library for a Dragon card, reveal it, put it into your hand, then shuffle.
−7: Destroy all non-Dragon creatures.
Sivitri, Dragon Master can be your commander.

Oath of Teferi - (Gatherer) (Scryfall) (EDHREC)

{3}{W}{U}
Legendary Enchantment
When Oath of Teferi enters, exile another target permanent you control. Return it to the battlefield under its owner's control at the beginning of the next end step.
You may activate the loyalty abilities of planeswalkers you control twice each turn rather than only once.

if (rEffect != null) {
if (consumed.containsKey(rEffect.getId())) {
Set<UUID> set = consumed.get(rEffect.getId());
if (consumed.containsKey(rEffect.getOriginalId())) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a bad solution. Instead research and fix wrong ApplyEffects usage or effects logic -- it's try to skip it. OriginalId concept for linked abilities, not for game engine.

Whole permanent logic with apply effects uses idempotency -- you must able to call it multiple times and catch same game state result. Not different.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also require review of original issue #10372 and unfinished changes with copy abilities ids in #11077. It can depends on each other.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the tapping of lands to pay for the tax that triggers the apply, see:

INFO  2026-05-22 01:41:02,088 [LOG][GAME] T2.DA: PlayerB using choice: Yes (by setChoice, ability: Archangel of Tithes [ed0]) =>[main] PrintGameLogsDataCollector.writeLog
INFO  2026-05-22 01:41:02,091 applying effects                                                                           =>[main] GameState.applyEffects
java.lang.Exception
        at mage.game.GameState.applyEffects(GameState.java:669)
        at mage.game.GameImpl.applyEffects(GameImpl.java:1975)
        at mage.abilities.AbilityImpl.activate(AbilityImpl.java:276)
        at mage.abilities.ActivatedAbilityImpl.activate(ActivatedAbilityImpl.java:227)
        at mage.abilities.Ability.activate(Ability.java:297)
        at mage.players.PlayerImpl.playManaAbility(PlayerImpl.java:1525)
        at mage.players.PlayerImpl.activateAbility(PlayerImpl.java:1669)
        at mage.player.ai.ComputerPlayer.playManaHandling(ComputerPlayer.java:578)
        at mage.player.ai.ComputerPlayer.playMana(ComputerPlayer.java:393)
        at org.mage.test.player.TestPlayer.playMana(TestPlayer.java:4415)
        at mage.abilities.costs.mana.ManaCostsImpl.pay(ManaCostsImpl.java:146)
        at mage.abilities.costs.mana.ManaCostsImpl.payOrRollback(ManaCostsImpl.java:173)
        at mage.abilities.effects.PayCostToAttackBlockEffectImpl.handleManaCosts(PayCostToAttackBlockEffectImpl.java:115)
        at mage.abilities.effects.PayCostToAttackBlockEffectImpl.replaceEvent(PayCostToAttackBlockEffectImpl.java:93)
        at mage.abilities.effects.ContinuousEffects.replaceEvent(ContinuousEffects.java:936)
        at mage.game.GameState.replaceEvent(GameState.java:1054)
        at mage.game.GameState.replaceEvent(GameState.java:1047)
        at mage.game.GameImpl.replaceEvent(GameImpl.java:3511)
        at mage.game.combat.Combat.declareAttacker(Combat.java:1468)
        at mage.players.PlayerImpl.declareAttacker(PlayerImpl.java:2863)
        at org.mage.test.player.TestPlayer.selectAttackers(TestPlayer.java:1978)
        at mage.game.combat.Combat.selectAttackers(Combat.java:271)
        at mage.game.turn.DeclareAttackersStep.beginStep(DeclareAttackersStep.java:36)
        at mage.game.turn.Phase.prePriority(Phase.java:189)
        at mage.game.turn.Phase.playStep(Phase.java:203)
        at mage.game.turn.Phase.play(Phase.java:91)
        at mage.game.turn.Turn.play(Turn.java:132)
        at mage.game.GameImpl.playTurn(GameImpl.java:1180)
        at mage.game.GameImpl.play(GameImpl.java:1087)
        at mage.game.GameImpl.start(GameImpl.java:1063)
        at org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.execute(CardTestPlayerAPIImpl.java:324)
        at org.mage.test.cards.copy.CloneTest.testCloneAttackTax(CloneTest.java:309)

so how do we go about this? tapping the lands is an ability being activated, so I do imagine this needs to be here to trigger abilities e.g. Manabarbs.

@matoro
Copy link
Copy Markdown
Contributor Author

matoro commented May 22, 2026

This seems like it could be a nice simple solution to those bugs, but I want to check a few things to confirm it's the right approach.

Can you reproduce #11560 before/after this change? There's a test case in #13465, not sure if could be impacted by the other recent Tyvar bugfix. Any opinion on whether deterministic id generating is still worth pursuing?

The first two Tyvar tests pass, but the Enduring Vitality one still does not.

I assume #10802 approach is still needed for Whipgrass Entangler, because that has the additional complexity that the ability can be gained multiple times, but in the becoming a copy case the ability is there exactly once?

I'll have to look at it, everything in this change should work fine with multiple copies of abilities.

But what about this example: Make [[Spark Double]] a copy of [[Sivitri, Dragon Master]] with [[Oath of Teferi]] to activate twice, does it work as expected?

Work correctly, pushing up a test to demonstrate.

matoro added 2 commits May 22, 2026 02:19
The root cause of the issue here is that when a permanent with a
replacement effect is copied, this is applied via a continuous
`CopyEffect`.  Normally this is not a problem, but for attack taxers,
part of choosing whether the replacement effect applies or not is
tapping mana sources, which triggers `applyEffects()` to run.  The
`CopyEffect` is then reapplied, and as part of it, it clears the copied
abilities and generates new ones with new IDs from the blueprint.  Thus,
the loop checking whether all replacement effects have been consumed
never terminates, because it sees a new replacement effect with a new ID
it needs to apply.

This extends the concept of `originalId`, which abilities already have,
to also apply to effects.  It can be used in any scenarios which need an
effect's ID to remain stable even when it is destroyed and recreated via
a copy from a continuous effect.

Fixes magefree#10372

Fixes magefree#14754
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG // Propaganda asks to pay again infinitly Repeat Glitch - Archangel of Tithes copied via Malleable Imposter

3 participants