Skip to content

Linked ability framework for Zinnia Valley's Voice#14625

Open
sneddigrolyat wants to merge 13 commits intomagefree:masterfrom
sneddigrolyat:zinnia-linked-ability
Open

Linked ability framework for Zinnia Valley's Voice#14625
sneddigrolyat wants to merge 13 commits intomagefree:masterfrom
sneddigrolyat:zinnia-linked-ability

Conversation

@sneddigrolyat
Copy link
Copy Markdown

@sneddigrolyat sneddigrolyat commented Mar 14, 2026

This PR targets the engine bug from #12752 that Zinnia exposed, not just the single card implementation in #12568.

The underlying issue was that linked additional-cost / ETB state for mechanics like Offspring was not staying correct across:

  • abilities granted to spells by other objects;
  • copied permanent spells;
  • the spell-to-permanent transition when a copied spell resolves as a token permanent.

Earlier attempts in #14595 and #14609 improved the symptom, but they still relied on workaround-ish handling and did not hold up cleanly for the broader linked-ability model.

This should also be reusable for future cards that grant a cost-linked spell ability whose ETB or copied trigger must remember which specific cost instance was paid; current adjacent patterns include granted Prowl, Blitz, and Freerunning, though only Offspring and Squad are explicitly migrated to linkage-aware tagging in this PR.

What changed

  • Added deterministic Ability.remapForSource(UUID) support so granted abilities can be copied onto cards/spells with a stable per-source identity and linkage instead of sharing raw ability state.
  • Updated GainAbilityControlledSpellsEffect to grant remapped ability copies, which keeps multiple granted Offspring instances independent.
  • Updated TriggeredAbilities.checkTrigger to prefer game.getPermanentEntering(sourceId) for ETB validation. This fixes copied permanent spells whose ETB validation could otherwise look at the copied card/spell object instead of the entering permanent.
  • Narrowed the copied-spell token path so a resolving copied permanent spell preserves the linked ability instances it already had during the spell-to-permanent transition.
  • Backed away from the broader copied-permanent remap experiment after it caused unrelated regressions in a full Mage.Tests sweep.

##Zinnia on the battlefield, cast a creature spell, choose Offspring, let it resolve, then watch the ETB trigger.

Flow

Zinnia static ability is active
-> GainAbilityControlledSpellsEffect.apply(...)
-> copies Offspring onto each matching creature card/spell
-> remapForSource(seed)
-> that granted ability gets a stable linkageId for this grant instance

You cast the creature spell
-> OffspringAbility.addOptionalAdditionalCosts(...)
-> if you pay {2}, the spell ability stores:
offspringActivation|

The creature spell resolves
-> Spell.resolve(...)
-> storePermanentCostsTags(stack-MOR -> paid-cost tags)
-> storeEnteringAbilities(future-permanent-MOR -> gained abilities)
-> move card from stack to battlefield

The first apply-effects pass after ETB
-> ZonesHandler creates PermanentCard and marks it as "entering"
-> GameState.applyEffects(...)
-> battlefield.reset(...)
-> applyEnteringAbilities(...)
-> the gained Offspring ETB ability is attached to the entering permanent

ETB trigger evaluation
-> TriggeredAbilities.checkTrigger(...)
-> uses game.getPermanentEntering(sourceId) first
-> OffspringTriggeredAbility.checkTrigger(...)
-> looks up offspringActivation|
-> if found, the ETB trigger is created

ETB trigger resolves
-> OffspringEffect.apply(...)
-> uses copied-trigger target pointer if present, else source permanent
-> creates the 1/1 token copy

The branch's actual insertion points are:

  • Ability identity/linkage:
    AbilityImpl.java:160
    CardUtil.java:1967
    id can change, but linkageId is what ties cast-time payment to the ETB ability.

  • Granted-ability handoff from stack to permanent:
    GainAbilityControlledSpellsEffect.java:45
    Spell.java:359
    GameState.java:1559
    PermanentCard.java:102

  • Trigger validation on ETB and copy cases:
    TriggeredAbilities.java:231
    OffspringAbility.java:161
    PermanentToken.java:101
    SpellAbility.java:263

Breakpoints

  1. ZinniaValleysVoice.java:69
  2. GainAbilityControlledSpellsEffect.java:91
  3. OffspringAbility.java:78
  4. Spell.java:359
  5. GameState.java:672 and GameState.java:1574
  6. TriggeredAbilities.java:231
  7. OffspringAbility.java:127

For copies, the divergence is:

copy spell on stack
-> SpellAbility.copySpell / Spell.copySpell
-> new runtime id, same linkageId
-> if it resolves as a token permanent:
PermanentToken(... preserveCopiedSpellLinkage=true)
-> addAbilityKeepingLinkage(...)

That is why copied Offspring/Squad spells and copied ETB triggers still read the right paid-cost state.

Squad is the same pipe, except it stores an integer count instead of a presence tag in SquadAbility.java:101.

Coverage

Added/updated regression coverage for:

  • two separate granted Offspring instances with only one payment;
  • copying a paid Offspring spell;
  • copying an Offspring ETB trigger;
  • rollback clearing stale printed+granted payment state;
  • copied Zinnia (Spark Double) with one granted payment and with two granted payments.

Validation

Automated:

  • OffspringTest passed
  • ZinniaValleysVoiceTest passed
  • SquadTest passed
  • MeldTest#testMeld_Urza_Eliminate_After_Rollback passed
  • JaceTest#rollbackDoesntUnflipJaceTest passed
  • StateValuesTest#rollbackTokenCreationTest passed

Latest confirmed broad run:

  • mvn -pl Mage.Tests -am test
  • Result: 6529 run, 1 failure, 0 errors, 124 skipped
  • Remaining failure: SimulationTriggersAITest.test_DeepglowSkate_PerformanceOnTooManyChoices
  • That failure also reproduced on master, so it does not appear to be caused by this branch

Manual:
March 19, 2026:

  • key Offspring/Zinnia sequencing scenarios passed
  • undo after wrong offspring payment passed
  • countered creature (no offspring token) passed
  • token copy of offspring permanent should not self-copy (Cackling Counterpart) passed
  • zone actions on original vs offspring (Unsummon / Path to Exile / Doom Blade) passed
  • combat and stack interactions passed
  • reconnect baseline
  • fast match flow baseline

Main review question

Does this narrower model for preserving linkage on granted abilities and copied permanent spells look like the right CR 607 layer for XMage, without reintroducing the broader copied-permanent regressions from the earlier experiment?

@sneddigrolyat
Copy link
Copy Markdown
Author

@ssk97 Thanks, your comment was helpful. Replying here since you noted it belongs on the PR.

I ended up changing the approach in a few important ways based on the concerns you raised.
The final version does not rely on storing raw ability references across object-copy boundaries. Instead, the granted-ability path now remaps copied abilities to a deterministic per-source identity/linkage, so separate granted instances stay independent without depending on shared Java-object references. That also made it possible to cover rollback-sensitive cases directly.

AbilityImpl point taken. Moved the remap hook onto Ability (remapForSource(UUID)), with AbilityImpl providing the shared implementation and StackAbility delegating.

Also backed away from the broader PermanentCard / copied-permanent remap experiment. A full Mage.Tests sweep showed that approach was too broad and caused unrelated regressions. The remaining permanent-side fix is narrower:

  • ETB trigger validation now prefers game.getPermanentEntering(sourceId) over the copied card/spell object
  • copied permanent spells that resolve as token permanents preserve the linked ability instances they already had through the spell-to-permanent transition

I added coverage for the cases you called out:

  • copying a paid Offspring spell
  • copying an Offspring ETB trigger
  • copied Zinnia via Spark Double with one granted payment and with two granted payments
  • rollback clearing stale printed+granted Offspring payment state

Current local validation:

  • OffspringTest, ZinniaValleysVoiceTest, SquadTest, and the rollback sanity tests passed
  • today’s manual tests also passed
  • latest broad Mage.Tests run had one remaining failure in SimulationTriggersAITest.test_DeepglowSkate_PerformanceOnTooManyChoices, and that same failure reproduced on master

The main thing I’d like review on is whether this narrower linkage-preservation model for granted abilities plus copied permanent spells is the right way to go.

@sneddigrolyat sneddigrolyat marked this pull request as ready for review March 21, 2026 03:39
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.

1 participant