You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Saruman of Many Colors' "Copy the exiled card" copies the wrong card (or nothing) at runtime. This is a distinct root cause from #4792 (fixed by #5571) and was deliberately scoped out of that PR.
Oracle text (verified from client/public/card-data.json)
Ward—Discard an enchantment, instant, or sorcery card.
Whenever you cast your second spell each turn, each opponent mills two cards. When one or more cards are milled this way, exile target enchantment, instant, or sorcery card with equal or lesser mana value than that spell from an opponent's graveyard. Copy the exiled card. You may cast the copy without paying its mana cost.
#5571 fixed the cross-resolution case, where "the exiled card" refers to an exile made by a different, earlier ability (Isochron Scepter's Imprint, Hideaway lands). It routes those to the durable TargetFilter::ExiledBySource link.
Saruman is same-chain: the exile (exile target … card … from an opponent's graveyard) and the Copy the exiled card live in the same triggered ability. #5571's exile-producer gate therefore (correctly) keeps Saruman's copy target as the chain-local TrackedSet(0) — it must not flip, or it would regress.
Parsed AST (current): CopySpell { target: TrackedSet(0) } — correct for a same-chain reference.
Actual root cause (runtime)
change_zone::resolve() never calls publish_tracked_set for a plain targetedEffect::ChangeZone { destination: Exile } — every write to chain_tracked_set_id / tracked_object_sets in that file is under #[cfg(test)]. So when Saruman's CopySpell { TrackedSet(0) } sub-ability resolves, TrackedSet(0) is empty and copy_source_from_tracked_set (game/effects/copy_spell.rs) falls back to latest_tracked_set_id — a global scan for any non-empty tracked set — which by that point is some unrelated set (or empty). Result: wrong card copied, or silent no-op.
(a) Runtime — publish a tracked set for exile-destination ChangeZone in change_zone::resolve(). NOT recommended: blast radius is every targeted ChangeZone{Exile} in the game (removal-exile, flicker, graveyard-exile — hundreds of cards), risking unrelated TrackedSet consumers / delayed-return snapshots.
(b) Narrow parser refinement — route same-chain plain-targetedChangeZone{Exile}-then-copy to ExiledBySource. Lower blast radius; reuses the exile_links path fix(parser): bind "the exiled card" anaphor to ExiledBySource for cross-resolution abilities (#4792, #3246, #3674) #5571 added (records via should_track_exiled_by_source, resolves via copy_source_from_exiled_by_source). But it also flips two other same-chain plain-targeted-exile cards whose runtime correctness under ExiledBySource is currently unverified and needs per-card runtime tests:
Evolving Door — {1},{T}, Sacrifice a creature: … Exile that card, then shuffle. You may cast the exiled card. (shuffle between exile and cast)
Beseech the Mirror — Search your library for a card, exile it face down, then shuffle. … you may cast the exiled card … Put the exiled card into your hand if it wasn't cast this way. (face-down + bargain + "put into hand" fallback)
Recommended: (b), gated behind runtime tests for Evolving Door and Beseech the Mirror to prove no regression.
References
Follow-up to #4792 / #3246 / #3674, split out of #5571. The runtime building block (ExiledBySource + exile_links + copy_source_from_exiled_by_source) already exists after #5571 — this issue is about wiring the same-chain-targeted-exile case to it and verifying the collateral cards.
Summary
Saruman of Many Colors' "Copy the exiled card" copies the wrong card (or nothing) at runtime. This is a distinct root cause from #4792 (fixed by #5571) and was deliberately scoped out of that PR.
Oracle text (verified from
client/public/card-data.json)Why this is NOT fixed by #5571
#5571 fixed the cross-resolution case, where "the exiled card" refers to an exile made by a different, earlier ability (Isochron Scepter's Imprint, Hideaway lands). It routes those to the durable
TargetFilter::ExiledBySourcelink.Saruman is same-chain: the exile (
exile target … card … from an opponent's graveyard) and theCopy the exiled cardlive in the same triggered ability. #5571's exile-producer gate therefore (correctly) keeps Saruman's copy target as the chain-localTrackedSet(0)— it must not flip, or it would regress.Parsed AST (current):
CopySpell { target: TrackedSet(0) }— correct for a same-chain reference.Actual root cause (runtime)
change_zone::resolve()never callspublish_tracked_setfor a plain targetedEffect::ChangeZone { destination: Exile }— every write tochain_tracked_set_id/tracked_object_setsin that file is under#[cfg(test)]. So when Saruman'sCopySpell { TrackedSet(0) }sub-ability resolves,TrackedSet(0)is empty andcopy_source_from_tracked_set(game/effects/copy_spell.rs) falls back tolatest_tracked_set_id— a global scan for any non-empty tracked set — which by that point is some unrelated set (or empty). Result: wrong card copied, or silent no-op.Candidate fixes (from the #5571 plan, §3)
ChangeZoneinchange_zone::resolve(). NOT recommended: blast radius is every targetedChangeZone{Exile}in the game (removal-exile, flicker, graveyard-exile — hundreds of cards), risking unrelatedTrackedSetconsumers / delayed-return snapshots.ChangeZone{Exile}-then-copy toExiledBySource. Lower blast radius; reuses theexile_linkspath fix(parser): bind "the exiled card" anaphor to ExiledBySource for cross-resolution abilities (#4792, #3246, #3674) #5571 added (records viashould_track_exiled_by_source, resolves viacopy_source_from_exiled_by_source). But it also flips two other same-chain plain-targeted-exile cards whose runtime correctness underExiledBySourceis currently unverified and needs per-card runtime tests:{1},{T}, Sacrifice a creature: … Exile that card, then shuffle. You may cast the exiled card.(shuffle between exile and cast)Search your library for a card, exile it face down, then shuffle. … you may cast the exiled card … Put the exiled card into your hand if it wasn't cast this way.(face-down + bargain + "put into hand" fallback)Recommended: (b), gated behind runtime tests for Evolving Door and Beseech the Mirror to prove no regression.
References
Follow-up to #4792 / #3246 / #3674, split out of #5571. The runtime building block (
ExiledBySource+exile_links+copy_source_from_exiled_by_source) already exists after #5571 — this issue is about wiring the same-chain-targeted-exile case to it and verifying the collateral cards.