fix(engine): resolve enters_under for any player ControllerRef (CR 110.2a) - #2817
Conversation
…0.2a)
resolve_enters_under_player only mapped ControllerRef::You to a concrete
PlayerId and returned Err("not yet supported") for every other variant, so
"put ... onto the battlefield under their/that/target player's control"
(ScopedPlayer, TargetPlayer, ParentTargetController, …) failed mid-resolution
for an entire class of cards (each-player / target-player mass put-into-play
and reanimation — Tempting Wurm, Show and Tell, Hypergenesis, Warp World).
Delegate to the canonical ControllerRef -> PlayerId resolver
(filter::controller_ref_player, now pub(crate)), threading state + ability.
None still means the owner's control; a ControllerRef that genuinely cannot
resolve to a single player in this context (e.g. bare Opponent) keeps the
explicit Err rather than silently guessing.
There was a problem hiding this comment.
Code Review
This pull request updates the zone-change resolution logic to support resolving arbitrary ControllerRef overrides (such as TargetPlayer and ScopedPlayer) via the canonical controller_ref_player resolver, aligning with CR 110.2a. It also adds corresponding unit tests. The review feedback points out a missing mandatory CR annotation comment on a rules-touching line of engine code in resolve_all, violating rule R6 of the style guide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let enters_under_player: Option<PlayerId> = match &ability.effect { | ||
| Effect::ChangeZoneAll { enters_under, .. } => { | ||
| resolve_enters_under_player("ChangeZoneAll", enters_under.as_ref(), ability.controller)? | ||
| resolve_enters_under_player(state, ability, "ChangeZoneAll", enters_under.as_ref())? |
There was a problem hiding this comment.
CR 110.2a: The call to resolve_enters_under_player in resolve_all is a rules-touching line of engine code but lacks a mandatory CR annotation comment. Adding a CR 110.2a comment here ensures compliance with the strict R6 rule of the style guide.
// CR 110.2a: Resolve the controller-override reference to a concrete PlayerId.
resolve_enters_under_player(state, ability, "ChangeZoneAll", enters_under.as_ref())?References
- R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form
CR <number>: <description>. (link)
Review: PR #2817 — resolve
|
Review: PR #2817 — resolve
|
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer sign-off: delegates enters_under resolution to the controller_ref_player single authority (exhaustive 9-variant, CR 110.2a); bug confirmed live on main; two independent approvals; Gemini nit refuted with evidence.
…hase-rs#2945) * feat(engine): enter under an opponent's control on ETB (CR 110.2a) Model the self-ETB replacement "<this permanent> enters under the control of an opponent of your choice" — Xantcha, Sleeper Agent; Captive Audience; Pendant of Prosperity; Abby, Merciless Soldier. Previously the clause was dropped (Effect:effect_structure gap) and the permanent entered under its owner's control. - types/ability.rs: ReplacementDefinition gains `enters_under: Option<ControllerRef>` (serde default/skip — card-data back-compatible), parallel to the imperative Effect::ChangeZone.enters_under slot. - game/replacement.rs: surface it as an EventModifiers field and stamp the entering ZoneChange's controller_override before ETB triggers fire (never under the owner first). Opponent resolves to the entering object's opponent (sole opponent in two-player; first in seat order otherwise). - parser/oracle_replacement.rs: emit the Moved/SelfRef/Battlefield/ enters_under=Opponent replacement for the self-control clause. Reuses the controller-resolution + entry pipeline generalized in phase-rs#2817. Fixes phase-rs#2834 * fix: add enters_under to ReplacementDefinition literals in dependent crates The new `ReplacementDefinition.enters_under` field broke the explicit struct literals in mtgish-import (convert/replacement.rs, 9 sites) and phase-ai (cast_facts.rs, 1 site), which enumerate every field. Add `enters_under: None` to each (the `..`-spread site in cast_facts is unaffected). Workspace builds and clippy -D warnings clean.
Summary
Fixes the engine side of #2813 (CR 110.2a).
resolve_enters_under_player(game/effects/change_zone.rs) only mappedControllerRef::Youto a concretePlayerIdand returnedErr("not yet supported")for every other variant. So any effect that puts a card onto the battlefield under a player other than the ability controller — "under their control" (each-player), "under target player's control", "that permanent's controller puts ..." — failed mid-resolution. This is a silent class gap: the card parses (theenters_undercarrier is produced), so coverage counted it "supported," butEffect::ChangeZone/Effect::ChangeZoneAllerrored at runtime.Affected class: each-player / target-player mass put-into-play and reanimation (Tempting Wurm, Show and Tell, Hypergenesis, Warp World, Scrambleverse, "under target player's control" reanimation, …).
Change
game/filter.rs— makecontroller_ref_player(the single authority forControllerRef -> PlayerId)pub(crate).game/effects/change_zone.rs—resolve_enters_under_playernow delegates tocontroller_ref_player, threadingstate+ability, so every player reference resolves consistently:You,ScopedPlayer(CR 115.10 / 608.2c),TargetPlayer(CR 109.4),ParentTargetController, etc. Both call sites (ChangeZone,ChangeZoneAll) updated.Nonestill means the owner's control. AControllerRefthat genuinely cannot resolve to a single player in this context (e.g. bareOpponentin multiplayer with no target) keeps the explicitErr— no silent guessing, exhaustive match, no new engine variant.Tests
enters_under_target_player_puts_card_under_chosen_player— card enters under the chosenTargetPlayer, not the ability controller.enters_under_scoped_player_uses_iterating_player— underplayer_scope, the card enters under the scoped (iterating) player.change_zone_all_strict_fails_on_unsupported_enters_under_controller_refstill passes, confirming the strict-fail invariant is preserved for genuinely-unresolvable refs.clippy -D warningsandfmtclean.Fixes #2813