Summary
Counters redirected onto a liminal token entrant lose the player who put them there. LiminalEntry::enter_with_counters is a Vec<(CounterType, u32)> with no actor slot, so the CR 122.6a placer is discarded at the redirect and the commit pass places the counters as somebody else.
The first case this is observable in is CR 702.104a Tribute on a token copy.
Where the actor is lost
add_counter_with_replacement receives an actor and, for a live object, threads it into ProposedEvent::AddCounter { placement: CounterPlacement::Object { actor, .. } }. For a liminal token entrant it takes the redirect instead:
crates/engine/src/game/effects/counters.rs:227 — if state.schedule_entry_counters(object_id, counter_type.clone(), count) { return true; }. The early return happens before actor is used.
crates/engine/src/types/game_state.rs — GameState::schedule_entry_counters(&mut self, id, counter_type, count) takes no actor and pushes (counter_type, count) onto entry.enter_with_counters.
crates/engine/src/game/effects/token.rs — the commit pass reads enter_with_counters back as Vec<(CounterType, u32)> (see token.rs:665, :1505, :1522), so there is nothing to restore the actor from.
The caller does supply a meaningful actor:
crates/engine/src/game/effects/tribute.rs:104 — apply_paid passes the paying opponent as actor.
Why the actor is not bookkeeping
CR 702.104a (docs/MagicCompRules.txt:4774):
Tribute is a static ability that functions as the creature with tribute is entering the battlefield. "Tribute N" means "As this creature enters, choose an opponent. That player may put an additional N +1/+1 counters on it as it enters."
The rule names the opponent as the placer, not the entering permanent's controller.
Replacement effects key on that. Vorinclex, Monstrous Raider:
If you would put one or more counters on a permanent or player, put twice that many of each of those kinds of counters on that permanent or player instead.
If an opponent would put one or more counters on a permanent or player, they put half that many of each of those kinds of counters on that permanent or player instead, rounded down.
So with a Vorinclex on the battlefield, a paid Tribute of N on a token copy should resolve to a halved or doubled count depending on who the placer is, and today the placer recorded is not the opponent CR 702.104a chose. GameEvent::CounterAdded trigger evaluation reads the same field.
Scope of the fix
enter_with_counters predates this seam — it has been actor-less since it was introduced, and the redirect in counters.rs is what first routes an actor-bearing call into it. Carrying the actor reaches:
- the
LiminalEntry / token-spec types that hold enter_with_counters (and their serialization),
GameState::schedule_entry_counters,
- every commit path that replays the list, including the resumed-entry path.
History
Found by CodeRabbit on #8606 and confirmed there. It is not a regression from that PR — before it, these counters were dropped entirely (apply_counter_addition found no object and returned true), so a paid tribute placed none at all. #8606 makes them land; this issue is the remaining attribution gap and was left out of that PR deliberately because the fix is a type change rather than a local correction.
Summary
Counters redirected onto a liminal token entrant lose the player who put them there.
LiminalEntry::enter_with_countersis aVec<(CounterType, u32)>with no actor slot, so the CR 122.6a placer is discarded at the redirect and the commit pass places the counters as somebody else.The first case this is observable in is CR 702.104a Tribute on a token copy.
Where the actor is lost
add_counter_with_replacementreceives anactorand, for a live object, threads it intoProposedEvent::AddCounter { placement: CounterPlacement::Object { actor, .. } }. For a liminal token entrant it takes the redirect instead:crates/engine/src/game/effects/counters.rs:227—if state.schedule_entry_counters(object_id, counter_type.clone(), count) { return true; }. The early return happens beforeactoris used.crates/engine/src/types/game_state.rs—GameState::schedule_entry_counters(&mut self, id, counter_type, count)takes no actor and pushes(counter_type, count)ontoentry.enter_with_counters.crates/engine/src/game/effects/token.rs— the commit pass readsenter_with_countersback asVec<(CounterType, u32)>(seetoken.rs:665,:1505,:1522), so there is nothing to restore the actor from.The caller does supply a meaningful actor:
crates/engine/src/game/effects/tribute.rs:104—apply_paidpasses the paying opponent asactor.Why the actor is not bookkeeping
CR 702.104a (
docs/MagicCompRules.txt:4774):The rule names the opponent as the placer, not the entering permanent's controller.
Replacement effects key on that. Vorinclex, Monstrous Raider:
So with a Vorinclex on the battlefield, a paid Tribute of N on a token copy should resolve to a halved or doubled count depending on who the placer is, and today the placer recorded is not the opponent CR 702.104a chose.
GameEvent::CounterAddedtrigger evaluation reads the same field.Scope of the fix
enter_with_counterspredates this seam — it has been actor-less since it was introduced, and the redirect incounters.rsis what first routes an actor-bearing call into it. Carrying the actor reaches:LiminalEntry/ token-spec types that holdenter_with_counters(and their serialization),GameState::schedule_entry_counters,History
Found by CodeRabbit on #8606 and confirmed there. It is not a regression from that PR — before it, these counters were dropped entirely (
apply_counter_additionfound no object and returnedtrue), so a paid tribute placed none at all. #8606 makes them land; this issue is the remaining attribution gap and was left out of that PR deliberately because the fix is a type change rather than a local correction.