Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions crates/engine/src/game/effects/pay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,6 @@ mod tests {
outcome,
ResolutionChoiceOutcome::WaitingFor(_)
| ResolutionChoiceOutcome::WaitingForWithInlineTriggers(_)
| ResolutionChoiceOutcome::WaitingForWithParkedObservers(_)
| ResolutionChoiceOutcome::ActionResult(_)
));
assert_eq!(state.players[0].life, 23);
Expand Down Expand Up @@ -1541,7 +1540,6 @@ mod tests {
match outcome {
ResolutionChoiceOutcome::WaitingFor(_) => {}
ResolutionChoiceOutcome::WaitingForWithInlineTriggers(_) => {}
ResolutionChoiceOutcome::WaitingForWithParkedObservers(_) => {}
ResolutionChoiceOutcome::ActionResult(_) => {}
}

Expand Down Expand Up @@ -1660,7 +1658,6 @@ mod tests {
outcome,
ResolutionChoiceOutcome::WaitingFor(_)
| ResolutionChoiceOutcome::WaitingForWithInlineTriggers(_)
| ResolutionChoiceOutcome::WaitingForWithParkedObservers(_)
| ResolutionChoiceOutcome::ActionResult(_)
));
assert_eq!(state.players[0].hand.len(), 2);
Expand Down Expand Up @@ -1807,7 +1804,6 @@ mod tests {
outcome,
ResolutionChoiceOutcome::WaitingFor(_)
| ResolutionChoiceOutcome::WaitingForWithInlineTriggers(_)
| ResolutionChoiceOutcome::WaitingForWithParkedObservers(_)
| ResolutionChoiceOutcome::ActionResult(_)
));
// All 7 mana units (4 colorless for X + W + U + B) must be spent —
Expand Down
14 changes: 5 additions & 9 deletions crates/engine/src/game/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6766,7 +6766,7 @@ fn apply_action(

let mut events = Vec::new();
let mut triggers_processed_inline = false;
let mut skip_deferred_trigger_drain = false;
let skip_deferred_trigger_drain = false;

// CancelAutoPass works from any WaitingFor state (player may cancel during
// interactive choices). Routed by `actor` — previously used
Expand Down Expand Up @@ -10402,13 +10402,6 @@ fn apply_action(
triggers_processed_inline = true;
waiting_for
}
engine_resolution_choices::ResolutionChoiceOutcome::WaitingForWithParkedObservers(
waiting_for,
) => {
triggers_processed_inline = true;
skip_deferred_trigger_drain = true;
waiting_for
}
engine_resolution_choices::ResolutionChoiceOutcome::ActionResult(result) => {
return Ok(result);
}
Expand Down Expand Up @@ -15887,7 +15880,10 @@ mod stage2_injector_tests {
// `scoped_library_search.rs`, neither of which this change touches, and the
// test module it adds contains no line matching the needle — total still 37,
// partition still 5/7/25.
"game/engine.rs:11828".to_string(),
// Search-observer dispatch: `:11828 ⇒ :11821`, −7. Removing the retired
// `WaitingForWithParkedObservers` match arm is the only hunk above this
// producer; it changes trigger-drain timing but does not add a prompt.
"game/engine.rs:11821".to_string(),
],
"the five production producers, NAMED: the CR 603.5 gate in `resolve_chain_body` \
plus the two repeated-optional-payment drivers, the per-player acceptance cursor \
Expand Down
23 changes: 9 additions & 14 deletions crates/engine/src/game/engine_resolution_choices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,7 @@ fn finish_search_found_batch(
events,
) {
ResolutionChoiceOutcome::WaitingFor(waiting)
| ResolutionChoiceOutcome::WaitingForWithInlineTriggers(waiting)
| ResolutionChoiceOutcome::WaitingForWithParkedObservers(waiting) => waiting,
| ResolutionChoiceOutcome::WaitingForWithInlineTriggers(waiting) => waiting,
ResolutionChoiceOutcome::ActionResult(result) => result.waiting_for,
},
)
Expand All @@ -506,10 +505,6 @@ fn finish_search_found_batch(
pub(super) enum ResolutionChoiceOutcome {
WaitingFor(WaitingFor),
WaitingForWithInlineTriggers(WaitingFor),
/// CR 603.3b: observer triggers from a completed search put/shuffle were
/// collected into `deferred_triggers` but must not drain until the caller
/// receives priority again (issue #5336: Kodama + Nature's Lore).
WaitingForWithParkedObservers(WaitingFor),
ActionResult(ActionResult),
}

Expand Down Expand Up @@ -616,9 +611,9 @@ fn batch_or_drain_observer_triggers(
}

/// CR 603.2 + CR 603.3b + CR 701.23: after a search tutor's put/shuffle
/// continuation drains, park ETB/dies/discards observers for the next priority
/// checkpoint instead of dispatching them while the test harness (or UI) may
/// still be inside the same `SelectCards` action (issue #5336).
/// continuation drains, collect ETB/dies/discards observers before this
/// `SelectCards` action reaches its priority checkpoint. The ordinary
/// post-action drain then puts them on the stack before priority is returned.
///
/// CR 603.2c: this slice spans the whole continuation drain, so it holds both
/// the delivery's logical zone-change owner's occurrences (already collected by
Expand All @@ -629,7 +624,7 @@ fn batch_or_drain_observer_triggers(
/// authority instead. That authority's ledger half applies to every event kind,
/// matching the generic priority scan. Without it a fetched land's landfall/ETB
/// observers fire twice.
fn park_search_observer_triggers(
fn collect_search_observer_triggers(
state: &mut GameState,
events: &[GameEvent],
events_before_drain: usize,
Expand All @@ -651,7 +646,7 @@ fn park_search_observer_triggers(
// shared carrier authority prove that every such frame has drained before
// retiring the parent and releasing its CR 400.7j self-move link.
super::engine::settle_resolving_stack_entry_after_continuation_resume(state);
ResolutionChoiceOutcome::WaitingForWithParkedObservers(state.waiting_for.clone())
ResolutionChoiceOutcome::WaitingForWithInlineTriggers(state.waiting_for.clone())
}

pub(super) fn handles(waiting_for: &WaitingFor) -> bool {
Expand Down Expand Up @@ -995,7 +990,7 @@ fn finalize_standard_search_selection(
// before (and instead of stranding) the ordinary rider.
super::engine::resume_pending_continuation_if_priority(state, events)
.expect("a settled search choice must resume its continuation");
park_search_observer_triggers(state, events, events_before_drain)
collect_search_observer_triggers(state, events, events_before_drain)
}

/// CR 800.4a + CR 701.23a: If the exact hidden zone backing an ordinary
Expand Down Expand Up @@ -3902,7 +3897,7 @@ pub(super) fn handle_resolution_choice(
set_priority(state, player);
super::engine::resume_pending_continuation_if_priority(state, events)
.expect("a settled search choice must resume its continuation");
return Ok(park_search_observer_triggers(
return Ok(collect_search_observer_triggers(
state,
events,
events_before_drain,
Expand Down Expand Up @@ -3973,7 +3968,7 @@ pub(super) fn handle_resolution_choice(
set_priority(state, player);
super::engine::resume_pending_continuation_if_priority(state, events)
.expect("a settled search choice must resume its continuation");
park_search_observer_triggers(state, events, events_before_partition)
collect_search_observer_triggers(state, events, events_before_partition)
}
(
WaitingFor::OutsideGameChoice {
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/game/triggers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8505,7 +8505,7 @@ pub(crate) fn filter_consumed_trigger_events(
/// NOT need this — a blanket `ZoneChanged` drop is equivalent there, and that is
/// what `engine_resolution_choices::batch_or_drain_observer_triggers`
/// (owner-bounded slice + `zone_changes_are_logically_owned`) and the resumed
/// `ChangeZone` drain in `effects/mod.rs` do. `park_search_observer_triggers`'
/// `ChangeZone` drain in `effects/mod.rs` do. `collect_search_observer_triggers`'
/// slice spans a whole continuation drain and can hold zone changes no owner
/// allocated a group for, so it must consult this instead.
///
Expand Down
33 changes: 33 additions & 0 deletions crates/engine/tests/integration/escape_tunnel_landfall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use engine::game::scenario::{GameScenario, P0};
use engine::types::card_type::{CoreType, Supertype};
use engine::types::phase::Phase;
use engine::types::zones::Zone;

const ESCAPE_TUNNEL_ORACLE: &str = "{T}, Sacrifice this land: Search your library for a basic land card, put it onto the battlefield tapped, then shuffle.\n{T}, Sacrifice this land: Target creature with power 2 or less can't be blocked this turn.";
const KAZANDU_NECTARPOT_ORACLE: &str =
"Landfall — Whenever a land you control enters, you gain 1 life.";

#[test]
fn escape_tunnel_search_land_enters_and_triggers_landfall() {
let mut scenario = GameScenario::new();
scenario.at_phase(Phase::PreCombatMain);
let tunnel = scenario
.add_land_from_oracle(P0, "Escape Tunnel", ESCAPE_TUNNEL_ORACLE)
.id();
scenario.add_creature_from_oracle(P0, "Kazandu Nectarpot", 1, 2, KAZANDU_NECTARPOT_ORACLE);
let forest = scenario.add_card_to_library_top(P0, "Forest");
let mut runner = scenario.build();
let forest_object = runner.state_mut().objects.get_mut(&forest).unwrap();
forest_object.card_types.core_types.push(CoreType::Land);
forest_object.card_types.supertypes.push(Supertype::Basic);
forest_object.base_card_types = forest_object.card_types.clone();

// CR 603.2 + CR 603.3: the fetched basic land's entry triggers landfall,
// which goes on the stack before priority and resolves after both players pass.
let outcome = runner.activate(tunnel, 0).search_first_legal().resolve();

outcome.assert_zone(&[tunnel], Zone::Graveyard);
outcome.assert_zone(&[forest], Zone::Battlefield);
assert!(outcome.state().objects[&forest].tapped);
outcome.assert_life_delta(P0, 1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,12 @@ fn kodama_natures_lore_forest_limits_hand_to_zero_mana_value() {

let mut runner = scenario.build();
let forest = seed_forest_on_library_top(&mut runner);
runner.cast(natures_lore).search_first_legal().resolve();
runner.cast(natures_lore).resolve();
runner
.act(GameAction::SelectCards {
cards: vec![forest],
})
.expect("selecting Nature's Lore's Forest must succeed");

assert_eq!(
runner.state().objects[&forest].zone,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ mod engine_invariants;
mod enlightened_tutor_regression;
mod equipment_etb_attach_parent_target;
mod ertai_trickery_counter_kicked;
mod escape_tunnel_landfall;
mod etali_primal_sickness_poison;
mod etrata_cloak_enters_under_cloaker_5944;
mod evelyn_regression;
Expand Down
Loading
Loading