Follow-up to #7099, which pinned delayed-trigger ParentTarget referents to their
ObjectIncarnationRef so a blinked object is no longer affected by a trigger that
snapshotted its previous incarnation (CR 400.7 / CR 603.7c).
That PR fixed the reported Goryo's Vengeance + Ephemerate interaction and a second
bug (a referent that died was being exiled out of the graveyard). It did not
close the whole class. Three residuals, in priority order.
1. 33 tracked-set cards still have the bug
Measured runtime coverage: 267 of 317 card/CDT pairs (84.2%). The 33-pair gap is
a distinct mechanism, not a missed handler.
bind_tracked_set_to_ability_chain (crates/engine/src/game/effects/delayed_trigger.rs)
recurses into bind_tracked_set_to_effect, whose Effect::ChangeZone arm ends in a
_ => TargetFilter::TrackedSet { id } catch-all and also replaces the variant with
ChangeZoneAll. That runs before the pin gate, so ParentTarget is erased and
the gate never sees an anaphor to pin. Every one of the 33 is a single-node chain
whose entire target field is replaced wholesale — none has a nested or sibling node
that could survive.
Affected cards include Eerie Interlude, Ghostway, Yorion Sky Nomad, Venser the
Sojourner, Touch the Spirit Realm.
Measurement, from the #7099 run:
|
|
uses_tracked_set: true |
46 of 317 pairs |
| already inside the 17-pair exclusion |
13 |
| inside "retained", so genuinely lost |
33 |
| of those 33, surviving the binder |
0 |
Two honesty caveats on that number. It was computed by simulating the binder over
the AST, not by observing the engine. And the loss is conditional on a tracked set
being live: tracked_set_id resolves via chain_tracked_set_id or the game-wide
latest_tracked_set_id fallback, so an unrelated earlier tracked set can also
trigger the rewrite. Treat 267 as a lower bound on coverage and 33 as an upper
bound on the loss.
The naive fix — hoisting the pin gate above the binder — was rejected during review:
it stamps pins on chains that then resolve by live scan, so pinned_object_targets_all_stale
can go true and suppress a working resolution. A real fix has to make the binder
preserve the anaphor rather than move the gate.
2. ParentTargetSlot slot-index validation (latent)
Raised by CodeRabbit on #7099. When slot 0 is stale but another pinned target is still
current, the all-stale guard does not fire, and a raw slot-indexed read can resolve
slot 0 to the re-entered object with the same ObjectId — which is exactly the CR 603.7c
violation the PR set out to fix.
Currently latent: the measured population of slot-indexed ParentTarget consumers is
zero, so no shipping card reaches it. It becomes live the moment one does. The
structural fix is per-slot incarnation validation instead of the all-or-nothing guard.
3. Test residual: flip_permanent.rs / transform_effect.rs
Also from CodeRabbit. Both handlers took early-return-only treatment rather than
target substitution, because substituting on an emptied list would fall through to
[] => ability.source_id and make the effect target its own source. The behavior
change (a declared non-SelfRef stale target now resolves as a no-op instead of
flipping the re-entered object) has no dedicated test; it rests on full-suite
regression only.
The same "regression-only" caveat applies to the six Tier C handlers in #7099
(discard.rs, effect.rs, phase_out.rs, deal_damage.rs, tap_untap.rs,
cast_from_zone.rs). cast_from_zone.rs was flagged highest-risk of that set.
Not in scope here
Two pre-existing clusters were deliberately left alone in #7099 so each lands as one
auditable pass, and are not tracked by this issue:
- A parser misparse: "sacrifice this creature" parses to
ParentTarget where it
should be SelfRef.
- A CR mis-citation cluster:
sacrifice.rs cites CR 701.17a for sacrifice in several
places; 701.17a is mill, and Sacrifice is CR 701.21.
Follow-up to #7099, which pinned delayed-trigger
ParentTargetreferents to theirObjectIncarnationRefso a blinked object is no longer affected by a trigger thatsnapshotted its previous incarnation (CR 400.7 / CR 603.7c).
That PR fixed the reported Goryo's Vengeance + Ephemerate interaction and a second
bug (a referent that died was being exiled out of the graveyard). It did not
close the whole class. Three residuals, in priority order.
1. 33 tracked-set cards still have the bug
Measured runtime coverage: 267 of 317 card/CDT pairs (84.2%). The 33-pair gap is
a distinct mechanism, not a missed handler.
bind_tracked_set_to_ability_chain(crates/engine/src/game/effects/delayed_trigger.rs)recurses into
bind_tracked_set_to_effect, whoseEffect::ChangeZonearm ends in a_ => TargetFilter::TrackedSet { id }catch-all and also replaces the variant withChangeZoneAll. That runs before the pin gate, soParentTargetis erased andthe gate never sees an anaphor to pin. Every one of the 33 is a single-node chain
whose entire
targetfield is replaced wholesale — none has a nested or sibling nodethat could survive.
Affected cards include Eerie Interlude, Ghostway, Yorion Sky Nomad, Venser the
Sojourner, Touch the Spirit Realm.
Measurement, from the #7099 run:
uses_tracked_set: trueTwo honesty caveats on that number. It was computed by simulating the binder over
the AST, not by observing the engine. And the loss is conditional on a tracked set
being live:
tracked_set_idresolves viachain_tracked_set_idor the game-widelatest_tracked_set_idfallback, so an unrelated earlier tracked set can alsotrigger the rewrite. Treat 267 as a lower bound on coverage and 33 as an upper
bound on the loss.
The naive fix — hoisting the pin gate above the binder — was rejected during review:
it stamps pins on chains that then resolve by live scan, so
pinned_object_targets_all_stalecan go true and suppress a working resolution. A real fix has to make the binder
preserve the anaphor rather than move the gate.
2.
ParentTargetSlotslot-index validation (latent)Raised by CodeRabbit on #7099. When slot 0 is stale but another pinned target is still
current, the all-stale guard does not fire, and a raw slot-indexed read can resolve
slot 0 to the re-entered object with the same
ObjectId— which is exactly the CR 603.7cviolation the PR set out to fix.
Currently latent: the measured population of slot-indexed
ParentTargetconsumers iszero, so no shipping card reaches it. It becomes live the moment one does. The
structural fix is per-slot incarnation validation instead of the all-or-nothing guard.
3. Test residual:
flip_permanent.rs/transform_effect.rsAlso from CodeRabbit. Both handlers took early-return-only treatment rather than
target substitution, because substituting on an emptied list would fall through to
[] => ability.source_idand make the effect target its own source. The behaviorchange (a declared non-
SelfRefstale target now resolves as a no-op instead offlipping the re-entered object) has no dedicated test; it rests on full-suite
regression only.
The same "regression-only" caveat applies to the six Tier C handlers in #7099
(
discard.rs,effect.rs,phase_out.rs,deal_damage.rs,tap_untap.rs,cast_from_zone.rs).cast_from_zone.rswas flagged highest-risk of that set.Not in scope here
Two pre-existing clusters were deliberately left alone in #7099 so each lands as one
auditable pass, and are not tracked by this issue:
ParentTargetwhere itshould be
SelfRef.sacrifice.rscitesCR 701.17afor sacrifice in severalplaces; 701.17a is mill, and Sacrifice is CR 701.21.