From 0a25c1359293482b03c6991a3f8562fa4cb758b0 Mon Sep 17 00:00:00 2001 From: cuinhellcat <217210902+cuinhellcat@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:27:25 +0200 Subject: [PATCH 1/4] fix(engine): publish the manifest-dread choice continuation's tracked set (#7467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manifest dread with two library cards parks WaitingFor::ManifestDreadChoice, so the manifested creature enters from the continuation handler and its ZoneChanged never reaches the resolver-side harvest — the chain's tracked set was published EMPTY when the head parked, and a chained TrackedSet consumer ("Manifest dread X times, then put X +1/+1 counters on each of those creatures") bound nothing. Cast with X > 0, the creatures got no counters. Re-publish the manifested object at the choice handler, the same seam and the same gate as the existing search-choice publishes (chain_references_tracked_set + actual battlefield arrival, mirroring the harvest's destination filter). Measured over card-data.json: exactly one catalog card chains a TrackedSet consumer after ManifestDread (Valgavoth's Onslaught). Known gaps (issue #7467 stays open): the paused-entry arm (an aura-host or replacement-ordering pause during the manifest entry defers through RevealRestPile, whose publish_tracked_set field doubles as a routing selector and is not touched here) and both DraftFromSpellbook rows. Co-Authored-By: Claude Fable 5 --- .../src/game/engine_resolution_choices.rs | 20 +++ .../issue_7467_manifest_dread_tracked_set.rs | 122 ++++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 3 files changed, 143 insertions(+) create mode 100644 crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs diff --git a/crates/engine/src/game/engine_resolution_choices.rs b/crates/engine/src/game/engine_resolution_choices.rs index bca587bcfb..ce61f3b633 100644 --- a/crates/engine/src/game/engine_resolution_choices.rs +++ b/crates/engine/src/game/engine_resolution_choices.rs @@ -1864,6 +1864,26 @@ pub(super) fn handle_resolution_choice( } } + // CR 608.2c + CR 701.62a (#7467): the manifested creature enters + // from THIS continuation, so its `ZoneChanged` never reaches the + // resolver-side harvest — the chain's tracked set was published + // EMPTY when the head parked. Re-publish it here so a chained + // consumer ("Manifest dread X times, then put X +1/+1 counters on + // each of those creatures" — Valgavoth's Onslaught) binds the + // creature, the same seam as the search-choice publish above. + // Gated identically (only a tracked-set-reading continuation), and + // on actual battlefield arrival, mirroring the harvest's + // destination filter: an entry replacement that redirected the + // card elsewhere manifested nothing. + let continuation_consumes_tracked_set = state + .active_ability_continuation() + .is_some_and(|continuation| { + effects::chain_references_tracked_set(&continuation.chain) + }); + if continuation_consumes_tracked_set && state.battlefield.contains(&manifest_id) { + effects::publish_fresh_tracked_set(state, vec![manifest_id]); + } + // CR 614.6 + CR 701.62a class: route the non-manifested cards to the // graveyard through the simultaneous-move batch so each card's own // `Moved` redirects (Rest in Peace / Leyline of the Void: "would be diff --git a/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs b/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs new file mode 100644 index 0000000000..b8f834b0c3 --- /dev/null +++ b/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs @@ -0,0 +1,122 @@ +//! #7467 (ManifestDread row): "Manifest dread X times, then put X +1/+1 +//! counters on each of those creatures" — Valgavoth's Onslaught. +//! +//! Manifest dread with two or more library cards parks +//! `WaitingFor::ManifestDreadChoice`, so the manifested creature enters from +//! the CONTINUATION handler and its `ZoneChanged` lands on the continuation's +//! event vector. The resolver-side tracked-set publish (CR 603.7 class, +//! `effects/mod.rs`) harvests only the resolver's own events — it published an +//! EMPTY set, and the chained `PutCounterAll { TrackedSet }` ("each of those +//! creatures") bound nothing. Cast with X > 0, the manifested creatures got no +//! counters at all. +//! +//! The catalog parse chains the counter sub-ability inside each `repeat_for` +//! iteration, so each creature receives its X counters before the next +//! iteration manifests — the FINAL board (every creature manifested this way +//! carries X counters) is what the printed text requires, and what these rows +//! measure. + +use engine::game::scenario::{GameRunner, GameScenario, P0}; +use engine::types::actions::GameAction; +use engine::types::counter::CounterType; +use engine::types::game_state::WaitingFor; +use engine::types::identifiers::ObjectId; +use engine::types::mana::{ManaCost, ManaCostShard, ManaType, ManaUnit}; +use engine::types::phase::Phase; + +const ONSLAUGHT: &str = + "Manifest dread X times, then put X +1/+1 counters on each of those creatures."; + +/// A fabricated Onslaught in hand ({X} cost) plus `library` library cards so +/// manifest dread has something to look at. +fn board(library: usize) -> (GameRunner, ObjectId) { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + for i in 0..library { + scenario.add_card_to_library_top(P0, &format!("Library {i}")); + } + let spell = { + let mut b = + scenario.add_spell_to_hand_from_oracle(P0, "Fabricated Onslaught", false, ONSLAUGHT); + b.with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::X], + generic: 0, + }); + b.id() + }; + let pool = (0..4) + .map(|_| ManaUnit::new(ManaType::Colorless, ObjectId(0), false, vec![])) + .collect(); + scenario.with_mana_pool(P0, pool); + (scenario.build(), spell) +} + +fn p1p1_counters(runner: &GameRunner, id: ObjectId) -> u32 { + runner.state().objects[&id] + .counters + .get(&CounterType::Plus1Plus1) + .copied() + .unwrap_or(0) +} + +/// The continuation arm (the #7467 gap): X=2 with four library cards parks the +/// two-card choice twice; each chosen creature must end with X = 2 counters. +#[test] +fn each_creature_manifested_through_the_choice_gets_x_counters() { + let (mut runner, spell) = board(4); + runner.cast(spell).x(2).resolve(); + runner.advance_until_stack_empty(); + + let mut manifested = Vec::new(); + for round in 0..2 { + let offered = match &runner.state().waiting_for { + WaitingFor::ManifestDreadChoice { cards, .. } => cards.clone(), + other => panic!("round {round}: expected the manifest dread choice, got {other:?}"), + }; + assert_eq!(offered.len(), 2, "round {round}: two cards to choose from"); + let pick = offered[0]; + runner + .act(GameAction::SelectCards { cards: vec![pick] }) + .expect("choose the card to manifest"); + manifested.push(pick); + runner.advance_until_stack_empty(); + } + + for &id in &manifested { + let obj = &runner.state().objects[&id]; + assert!( + obj.face_down, + "the chosen card must sit face down on the battlefield" + ); + assert_eq!( + p1p1_counters(&runner, id), + 2, + "X=2: every creature manifested this way must carry X +1/+1 counters \ + (#7467: the choice continuation published no tracked set)" + ); + } +} + +/// The synchronous arm (control): a one-card library skips the choice — +/// `manifest_dread.rs` manifests directly and the resolver-side harvest +/// already publishes the set. Exactly X = 1 counter, before AND after the +/// continuation publish exists: the fix must not double-apply here. +#[test] +fn a_one_card_library_manifests_synchronously_with_exactly_x_counters() { + let (mut runner, spell) = board(1); + runner.cast(spell).x(1).resolve(); + runner.advance_until_stack_empty(); + + let manifested = runner + .state() + .battlefield + .iter() + .copied() + .find(|id| runner.state().objects[id].face_down) + .expect("the single library card must be manifested without a choice"); + assert_eq!( + p1p1_counters(&runner, manifested), + 1, + "X=1 on the synchronous arm: exactly one counter — no more (double publish), no less" + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index b863523eba..a230fadf1f 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -1219,6 +1219,7 @@ mod issue_4945_zada_hedron_grinder; mod issue_4948_samwise_gamgee_sacrifice_target_order; mod issue_5263_chaos_warp; mod issue_6367_thassas_oracle; +mod issue_7467_manifest_dread_tracked_set; mod issue_7552_role_token_image_ref; mod kang_dynasty_until_next_turn_rider; mod karplusan_yeti_fight_back; From 8826a4d48ba22039212f897efa08b1d9d5136050 Mon Sep 17 00:00:00 2001 From: cuinhellcat <217210902+cuinhellcat@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:44:06 +0200 Subject: [PATCH 2/4] fix(engine): publish the manifested object after a paused entry completes (#7563 round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review blocker: ManifestDreadChoice defers RevealRestPile on ZoneMoveResult::NeedsChoice / NeedsAuraAttachmentChoice, and the round-1 publish ran only in the Done arm — an as-enters or replacement-ordering pause still dropped the tracked set. The completion now carries the chosen object in a dedicated manifested_for_continuation field and the drain publishes it right before finish_with_continuation — only once the object actually sits on the battlefield. Deliberately NOT the existing publish_tracked_set field: its presence doubles as the dig-vs-reveal routing selector for the rest pile and rewrites the continuation's targets. The gate+battlefield publish is extracted to effects::publish_battlefield_object_for_pending_continuation, shared by the synchronous arm and the drain. New test forces the pause via two materially colliding enter-tap-state replacements (CR 616.1) and measures the counter landing; red before (0), green after. Also drops a wrong CR 603.7 citation from the round-1 test header. Co-Authored-By: Claude Fable 5 --- crates/engine/src/game/effects/mod.rs | 19 +++ .../engine/src/game/effects/reveal_until.rs | 4 + .../src/game/engine_resolution_choices.rs | 33 ++++-- crates/engine/src/types/game_state.rs | 10 ++ .../issue_7467_manifest_dread_tracked_set.rs | 108 +++++++++++++++++- 5 files changed, 160 insertions(+), 14 deletions(-) diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 288cf51a19..67614a7d94 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -6669,6 +6669,25 @@ pub(crate) fn publish_fresh_tracked_set( set_id } +/// CR 608.2c + CR 701.62a (#7467): publish `object_id` as the chain's fresh +/// tracked set iff the parked continuation actually reads one and the object +/// really sits on the battlefield — mirroring the resolver harvest's +/// destination filter, so an entry replacement that redirected the card +/// elsewhere publishes nothing. Shared by the two seams a manifest-dread +/// creature can finish entering from: the synchronous `ManifestDreadChoice` +/// arm and the paused-entry `RevealRestPile` completion. +pub(crate) fn publish_battlefield_object_for_pending_continuation( + state: &mut GameState, + object_id: ObjectId, +) { + let continuation_consumes_tracked_set = state + .active_ability_continuation() + .is_some_and(|continuation| chain_references_tracked_set(&continuation.chain)); + if continuation_consumes_tracked_set && state.battlefield.contains(&object_id) { + publish_fresh_tracked_set(state, vec![object_id]); + } +} + /// CR 603.7 + CR 109.5: Returns `true` when the effect resolves an acting /// subject relative to the parent target — i.e., any effect-target slot /// reachable via [`effect_target_filter`] contains diff --git a/crates/engine/src/game/effects/reveal_until.rs b/crates/engine/src/game/effects/reveal_until.rs index 839236551a..acbf6ad840 100644 --- a/crates/engine/src/game/effects/reveal_until.rs +++ b/crates/engine/src/game/effects/reveal_until.rs @@ -264,6 +264,7 @@ pub fn resolve( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: Some(ability.source_id), + manifested_for_continuation: None, }, ); return Ok(()); @@ -315,6 +316,7 @@ pub fn resolve( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: Some(ability.source_id), + manifested_for_continuation: None, }, ); return Ok(()); @@ -349,6 +351,7 @@ pub fn resolve( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: Some(ability.source_id), + manifested_for_continuation: None, }, ); return Ok(()); @@ -390,6 +393,7 @@ pub fn resolve( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: Some(ability.source_id), + manifested_for_continuation: None, }, ); return Ok(()); diff --git a/crates/engine/src/game/engine_resolution_choices.rs b/crates/engine/src/game/engine_resolution_choices.rs index ce61f3b633..a6f32bf138 100644 --- a/crates/engine/src/game/engine_resolution_choices.rs +++ b/crates/engine/src/game/engine_resolution_choices.rs @@ -1856,6 +1856,10 @@ pub(super) fn handle_resolution_choice( clear_markers: cards.clone(), publish_tracked_set: None, emit_reveal_until_resolved: None, + // #7467 review round 2: the entry paused, so the + // publish below never runs — the completion drain + // publishes instead, once the entry completed. + manifested_for_continuation: Some(manifest_id), }, ); return Ok(ResolutionChoiceOutcome::WaitingFor( @@ -1871,18 +1875,7 @@ pub(super) fn handle_resolution_choice( // consumer ("Manifest dread X times, then put X +1/+1 counters on // each of those creatures" — Valgavoth's Onslaught) binds the // creature, the same seam as the search-choice publish above. - // Gated identically (only a tracked-set-reading continuation), and - // on actual battlefield arrival, mirroring the harvest's - // destination filter: an entry replacement that redirected the - // card elsewhere manifested nothing. - let continuation_consumes_tracked_set = state - .active_ability_continuation() - .is_some_and(|continuation| { - effects::chain_references_tracked_set(&continuation.chain) - }); - if continuation_consumes_tracked_set && state.battlefield.contains(&manifest_id) { - effects::publish_fresh_tracked_set(state, vec![manifest_id]); - } + effects::publish_battlefield_object_for_pending_continuation(state, manifest_id); // CR 614.6 + CR 701.62a class: route the non-manifested cards to the // graveyard through the simultaneous-move batch so each card's own @@ -2116,6 +2109,7 @@ pub(super) fn handle_resolution_choice( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: None, + manifested_for_continuation: None, }, ); return Ok(ResolutionChoiceOutcome::WaitingFor( @@ -2180,6 +2174,7 @@ pub(super) fn handle_resolution_choice( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: None, + manifested_for_continuation: None, }), events, ) { @@ -3539,6 +3534,7 @@ pub(super) fn handle_resolution_choice( clear_markers: Vec::new(), publish_tracked_set: None, emit_reveal_until_resolved: None, + manifested_for_continuation: None, }, ); return Ok(ResolutionChoiceOutcome::WaitingFor( @@ -3666,6 +3662,7 @@ pub(super) fn handle_resolution_choice( clear_markers: Vec::new(), publish_tracked_set: Some(kept.clone()), emit_reveal_until_resolved: None, + manifested_for_continuation: None, }, ); return Ok(ResolutionChoiceOutcome::WaitingFor( @@ -3728,6 +3725,7 @@ pub(super) fn handle_resolution_choice( clear_markers: Vec::new(), publish_tracked_set: Some(publish_set), emit_reveal_until_resolved: None, + manifested_for_continuation: None, }, ); return Ok(ResolutionChoiceOutcome::WaitingFor( @@ -7347,6 +7345,7 @@ fn route_kept_card_or_defer( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved: None, + manifested_for_continuation: None, }, ); Some(ResolutionChoiceOutcome::WaitingFor( @@ -8019,6 +8018,7 @@ pub(crate) fn run_batch_completion( clear_markers, publish_tracked_set, emit_reveal_until_resolved, + manifested_for_continuation, } => { // The dig path (`publish_tracked_set.is_some()`) routes the rest pile // through `route_rest_partition` (ordered library bottom); the @@ -8047,6 +8047,7 @@ pub(crate) fn run_batch_completion( clear_markers, publish_tracked_set, emit_reveal_until_resolved, + manifested_for_continuation, }, ); return crate::game::zone_pipeline::BatchMoveResult::NeedsChoice; @@ -8067,6 +8068,7 @@ pub(crate) fn run_batch_completion( clear_markers, publish_tracked_set: None, emit_reveal_until_resolved, + manifested_for_continuation, }; return effects::reveal_until::move_rest_then( state, @@ -8099,6 +8101,13 @@ pub(crate) fn run_batch_completion( subject: None, }); } + // CR 608.2c + CR 701.62a (#7467): the paused manifest entry has + // completed by now — publish its object for the parked consumer, + // the deferred mirror of the synchronous `ManifestDreadChoice` + // publish (same gate, same battlefield filter). + if let Some(manifested) = manifested_for_continuation { + effects::publish_battlefield_object_for_pending_continuation(state, manifested); + } finish_with_continuation(state, player, events); crate::game::zone_pipeline::BatchMoveResult::Done } diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 7e4dc6d60b..89dd15dfb1 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -5338,6 +5338,16 @@ pub enum BatchCompletion { /// must too. `None` for the kept-choice / dig paths, which emit their own /// `EffectResolved` before the pause (or rely on the continuation). emit_reveal_until_resolved: Option, + /// CR 608.2c + CR 701.62a (#7467): a paused manifest-dread entry's + /// chosen object. The completion drain publishes it as the chain's + /// fresh tracked set — only once the entry has actually finished + /// (battlefield gate) and right before the parked consumer drains — + /// the deferred mirror of the synchronous `ManifestDreadChoice` + /// publish. Deliberately separate from `publish_tracked_set`, whose + /// presence doubles as the dig-vs-reveal ROUTING selector for the rest + /// pile. `None` for every non-manifest rest pile. + #[serde(default)] + manifested_for_continuation: Option, }, /// CR 608.2c + CR 616.1: The rest half of a deterministic mass Dig settled /// after a replacement choice. Resume its selected-card delivery only now, diff --git a/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs b/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs index b8f834b0c3..cfb92e100a 100644 --- a/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs +++ b/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs @@ -4,8 +4,8 @@ //! Manifest dread with two or more library cards parks //! `WaitingFor::ManifestDreadChoice`, so the manifested creature enters from //! the CONTINUATION handler and its `ZoneChanged` lands on the continuation's -//! event vector. The resolver-side tracked-set publish (CR 603.7 class, -//! `effects/mod.rs`) harvests only the resolver's own events — it published an +//! event vector. The resolver-side tracked-set publish (`effects/mod.rs`) +//! harvests only the resolver's own events — it published an //! EMPTY set, and the chained `PutCounterAll { TrackedSet }` ("each of those //! creatures") bound nothing. Cast with X > 0, the manifested creatures got no //! counters at all. @@ -120,3 +120,107 @@ fn a_one_card_library_manifests_synchronously_with_exactly_x_counters() { "X=1 on the synchronous arm: exactly one counter — no more (double publish), no less" ); } + +/// The paused-entry arm (review round 2): two opposite-direction enter +/// tap-state replacements collide materially (CR 616.1 — Tap vs Untap write +/// different final values), so the affected player must order them and the +/// manifest ENTRY itself parks at `ReplacementChoice`. The graveyard move and +/// the counter continuation defer through `BatchCompletion::RevealRestPile`; +/// the manifested creature must still end with its X = 1 counter once the +/// ordering is answered. +#[test] +fn a_paused_manifest_entry_still_feeds_the_counter_continuation() { + use engine::types::ability::{ + AbilityDefinition, AbilityKind, Effect, ReplacementDefinition, TapStateChange, TargetFilter, + }; + use engine::types::replacements::ReplacementEvent; + use engine::types::zones::Zone; + + fn enter_tap_state_battlefield_replacement( + description: &str, + state: TapStateChange, + ) -> ReplacementDefinition { + ReplacementDefinition::new(ReplacementEvent::Moved) + .destination_zone(Zone::Battlefield) + .execute(AbilityDefinition::new( + AbilityKind::Spell, + Effect::SetTapState { + target: TargetFilter::SelfRef, + scope: engine::types::ability::EffectScope::Single, + state, + }, + )) + .description(description.to_string()) + } + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let other = scenario.add_card_to_library_top(P0, "Other Card"); + let manifest = scenario.add_card_to_library_top(P0, "Manifest Me"); + scenario + .add_creature(engine::game::scenario::P1, "Kismet", 0, 0) + .as_enchantment() + .with_replacement_definition(enter_tap_state_battlefield_replacement( + "Creatures enter the battlefield tapped.", + TapStateChange::Tap, + )); + scenario + .add_creature(engine::game::scenario::P1, "Spelunking", 0, 0) + .as_enchantment() + .with_replacement_definition(enter_tap_state_battlefield_replacement( + "Permanents enter the battlefield untapped.", + TapStateChange::Untap, + )); + let spell = { + let mut b = + scenario.add_spell_to_hand_from_oracle(P0, "Fabricated Onslaught", false, ONSLAUGHT); + b.with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::X], + generic: 0, + }); + b.id() + }; + let pool = (0..2) + .map(|_| ManaUnit::new(ManaType::Colorless, ObjectId(0), false, vec![])) + .collect(); + scenario.with_mana_pool(P0, pool); + let mut runner = scenario.build(); + + runner.cast(spell).x(1).resolve(); + runner.advance_until_stack_empty(); + runner + .act(GameAction::SelectCards { + cards: vec![manifest], + }) + .expect("choose the card to manifest"); + assert!( + matches!( + runner.state().waiting_for, + engine::types::game_state::WaitingFor::ReplacementChoice { .. } + ), + "manifest entry must pause on the enter-tapped collision, got {:?}", + runner.state().waiting_for + ); + + runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("answer the enter-tapped ordering"); + runner.advance_until_stack_empty(); + + assert_eq!( + runner.state().objects[&manifest].zone, + Zone::Battlefield, + "the chosen card must finish manifesting" + ); + assert_eq!( + runner.state().objects[&other].zone, + Zone::Graveyard, + "CR 701.62a: the other looked-at card lands in the graveyard after the entry completes" + ); + assert_eq!( + p1p1_counters(&runner, manifest), + 1, + "X=1 through the PAUSED entry: the deferred completion must still publish \ + the manifested creature for the counter continuation (#7563 review round 2)" + ); +} From 42632e4b99fa5a85262207d200d0396fb549fa4f Mon Sep 17 00:00:00 2001 From: matthewevans Date: Thu, 20 Aug 2026 03:04:01 -0700 Subject: [PATCH 3/4] style(PR-7563): move test imports to module scope --- .../issue_7467_manifest_dread_tracked_set.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs b/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs index cfb92e100a..f392cf81ff 100644 --- a/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs +++ b/crates/engine/tests/integration/issue_7467_manifest_dread_tracked_set.rs @@ -17,12 +17,18 @@ //! measure. use engine::game::scenario::{GameRunner, GameScenario, P0}; +use engine::types::ability::{ + AbilityDefinition, AbilityKind, Effect, EffectScope, ReplacementDefinition, TapStateChange, + TargetFilter, +}; use engine::types::actions::GameAction; use engine::types::counter::CounterType; use engine::types::game_state::WaitingFor; use engine::types::identifiers::ObjectId; use engine::types::mana::{ManaCost, ManaCostShard, ManaType, ManaUnit}; use engine::types::phase::Phase; +use engine::types::replacements::ReplacementEvent; +use engine::types::zones::Zone; const ONSLAUGHT: &str = "Manifest dread X times, then put X +1/+1 counters on each of those creatures."; @@ -130,12 +136,6 @@ fn a_one_card_library_manifests_synchronously_with_exactly_x_counters() { /// ordering is answered. #[test] fn a_paused_manifest_entry_still_feeds_the_counter_continuation() { - use engine::types::ability::{ - AbilityDefinition, AbilityKind, Effect, ReplacementDefinition, TapStateChange, TargetFilter, - }; - use engine::types::replacements::ReplacementEvent; - use engine::types::zones::Zone; - fn enter_tap_state_battlefield_replacement( description: &str, state: TapStateChange, @@ -146,7 +146,7 @@ fn a_paused_manifest_entry_still_feeds_the_counter_continuation() { AbilityKind::Spell, Effect::SetTapState { target: TargetFilter::SelfRef, - scope: engine::types::ability::EffectScope::Single, + scope: EffectScope::Single, state, }, )) From 4dbd39f29e31f3dfdfb662f4f6cf0fcb8f2b82d1 Mon Sep 17 00:00:00 2001 From: cuinhellcat <217210902+cuinhellcat@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:28:38 +0200 Subject: [PATCH 4/4] test: re-pin the CR 603.5 prompt census after the effects helper shifted lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The census pins exact file:line producer addresses; the round-2 publish_battlefield_object_for_pending_continuation helper moved the three effects/mod.rs producers down by 19 lines. Same producers, new addresses — verified each is still its named OptionalEffectChoice construction. Co-Authored-By: Claude Fable 5 --- crates/engine/src/game/engine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 1554474c27..42a27dbd9b 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -19347,9 +19347,9 @@ mod stage2_injector_tests { // resume/finalization helpers are above this existing producer; // they do not mint an optional-effect prompt. The census above // still finds exactly the same five production producers. - "game/effects/mod.rs:7325".to_string(), - "game/effects/mod.rs:7402".to_string(), - "game/effects/mod.rs:11229".to_string(), + "game/effects/mod.rs:7344".to_string(), + "game/effects/mod.rs:7421".to_string(), + "game/effects/mod.rs:11248".to_string(), // UNMOVED across the rebase, and that is itself evidence the SET did not // move: a census that had gained or lost a producer would not leave this // entry both byte-identical AND at the same coordinate.