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
32 changes: 16 additions & 16 deletions crates/engine/src/game/effects/conjure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,7 @@ pub fn resolve(
}
}

// Record battlefield entry for restriction tracking.
if destination == Zone::Battlefield {
crate::game::restrictions::record_battlefield_entry(state, obj_id);
// Battlefield entry: incremental re-derive candidate for this
// conjured object (escalates to Full if it sources effects/etc.).
crate::game::layers::mark_layers_entered(state, obj_id);
Expand All @@ -202,20 +200,22 @@ pub fn resolve(
// (e.g. Verdant Dread's "another Verdant Dread enters" manifest-dread
// trigger, Soul Warden, Panharmonicon). Without this the conjured
// permanent enters silently and no ETB ability ever triggers.
let zone_change_record = state
.objects
.get(&obj_id)
.expect("conjured object was just created")
.snapshot_for_zone_change(obj_id, None, Zone::Battlefield);
state
.zone_changes_this_turn
.push_back(zone_change_record.clone());
events.push(GameEvent::ZoneChanged {
object_id: obj_id,
from: None,
to: Zone::Battlefield,
record: Box::new(zone_change_record),
});
//
// Conjuring is an Alchemy/Arena digital-only mechanic with NO
// Comprehensive Rules entry β€” the string "conjure" does not occur in the
// CR. The rules cited here are the ones the operation borrows: CR 400.7
// (the zone change), CR 608.2i (the battlefield-entry bookkeeping),
// CR 603.2c + CR 603.6a (why the index is load-bearing for batched ETB
// triggers).
//
// CR 400.7 + CR 608.2i + CR 603.2c: route the record and the emit through
// the single `from: None β†’ Battlefield` authority so the emitted record
// carries this turn's real zone-change index instead of the `0`
// placeholder, and so the CR 608.2i battlefield-entry row is written
// exactly once (the authority calls `record_battlefield_entry` itself β€”
// a co-located second call here would double-count it).
crate::game::zones::record_and_emit_entry_from_no_zone(state, obj_id, events)
.expect("conjured object was just created");
}

events.push(GameEvent::ObjectConjured {
Expand Down
569 changes: 516 additions & 53 deletions crates/engine/src/game/effects/counters.rs

Large diffs are not rendered by default.

30 changes: 13 additions & 17 deletions crates/engine/src/game/effects/gift_delivery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,29 +154,25 @@ fn create_gift_token(
}

crate::game::layers::mark_layers_full(state);
crate::game::restrictions::record_battlefield_entry(state, obj_id);
crate::game::restrictions::record_token_created(state, obj_id);

// CR 111.1 + CR 603.6a: Token creation is a zone change from outside the
// game β€” emit `ZoneChanged { from: None }` so ETB triggers (Soul Warden,
// Panharmonicon, etc.) fire for gift tokens through the normal code path.
let zone_change_record = state
.objects
.get(&obj_id)
.expect("token just created")
.snapshot_for_zone_change(obj_id, None, Zone::Battlefield);
events.push(GameEvent::ZoneChanged {
object_id: obj_id,
from: None,
to: Zone::Battlefield,
record: Box::new(zone_change_record),
});

events.push(GameEvent::TokenCreated {
object_id: obj_id,
name: name.to_string(),
//
// CR 400.7 + CR 608.2i + CR 603.2c: route the record and the entry pair through the single
// `from: None β†’ Battlefield` authority so the emitted `ZoneChanged` carries this turn's real
// zone-change index instead of the `0` placeholder. The authority performs the CR 608.2i
// battlefield-entry bookkeeping itself, so the co-located `record_battlefield_entry` call is
// deleted β€” keeping it would double-count `battlefield_entries_this_turn`.
super::token::push_committed_token_entry_events(
state,
obj_id,
name.to_string(),
source_id,
});
events,
)
.expect("token just created");

obj_id
}
Expand Down
37 changes: 16 additions & 21 deletions crates/engine/src/game/effects/incubate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub fn resolve(
// token (escalates to a full pass if it sources effects, carries
// counters, etc.).
crate::game::layers::mark_layers_entered(state, obj_id);
// CR 403.3 battlefield-entry bookkeeping is done by `record_zone_change` below β€”
// CR 608.2i battlefield-entry bookkeeping is done by `record_zone_change` below β€”
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// recording it here too would double-count `battlefield_entries_this_turn`.
crate::game::restrictions::record_token_created(state, obj_id);

Expand All @@ -111,26 +111,21 @@ pub fn resolve(
// `token.rs::apply_create_token_after_replacement_with_created_ids` and
// `conjure.rs`'s identical fix for the same bug class.
//
// CR 400.7 + CR 603.2c: route the record through `restrictions::record_zone_change` β€” the
// single authority that assigns this turn's zone-change index β€” and write the assigned index
// back onto the emitted record. `snapshot_for_zone_change` leaves it at its `0` placeholder,
// and the batched zone-change replay guard (`triggers.rs`) dedups on
// `(definition_ref, turn_zone_change_index)` read off the EVENT, so an unrouted record aliases
// this Incubator onto occurrence `0` and a `batched: true` ETB trigger that already fired for
// another entry this turn is swallowed. Same shape as `merge.rs` and `token.rs`.
let mut zone_change_record = state
.objects
.get(&obj_id)
.expect("incubator token was just created")
.snapshot_for_zone_change(obj_id, None, Zone::Battlefield);
zone_change_record.turn_zone_change_index =
crate::game::restrictions::record_zone_change(state, zone_change_record.clone());
events.push(GameEvent::ZoneChanged {
object_id: obj_id,
from: None,
to: Zone::Battlefield,
record: Box::new(zone_change_record),
});
// CR 400.7 + CR 608.2i + CR 603.2c: route the record and the emit through
// `zones::record_and_emit_entry_from_no_zone` β€” the single `from: None β†’ Battlefield`
// authority, which assigns this turn's zone-change index through
// `restrictions::record_zone_change` and writes it back onto the record it emits.
// That one call writes BOTH ledgers, which is why both rules are cited here: the CR 400.7
// zone-change row (whose length IS the index allocator) and β€” because `to_zone` is
// `Battlefield` β€” the CR 608.2i battlefield-entry row, via `record_battlefield_entry`. The
// latter is the look-back journal that a permanent which has since left still counts in, so
// re-recording either ledger at this call site would double-count it.
// `snapshot_for_zone_change` leaves that index at its `0` placeholder, and the batched
// zone-change replay guard (`triggers.rs`) dedups on `(definition_ref, turn_zone_change_index)`
// read off the EVENT, so an unrouted record aliases this Incubator onto occurrence `0` and a
// `batched: true` ETB trigger that already fired for another entry this turn is swallowed.
crate::game::zones::record_and_emit_entry_from_no_zone(state, obj_id, events)
.expect("incubator token was just created");

super::token::inject_predefined_token_abilities(state, obj_id);

Expand Down
Loading
Loading