1pub mod amount;
9pub mod card_script;
10pub mod compare;
11pub mod cost;
12pub mod keys;
13
14use std::collections::BTreeMap;
15use std::sync::{Mutex, OnceLock};
16
17use forge_foundation::ZoneType;
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19
20pub use amount::AmountExpr;
21pub use card_script::{
22 parse_semantic_param_value, ParamDiagnostic, ParamDiagnosticKind, ParamEntry, ParsedCardScript,
23 ParsedParams, ParsedParamsReport, ScriptAbility, ScriptAbilityRecord, ScriptDiagnostic,
24 ScriptDiagnosticKind, ScriptField, ScriptLine, ScriptLineKind, ScriptParamRecord, ScriptSVar,
25 ScriptSVarNumericExpression, ScriptSVarObjectRef, ScriptSVarValue, SemanticAmount,
26 SemanticComparison, SemanticComparisonOperator, SemanticParam, SemanticParamValue,
27 SemanticParamValueKind, SemanticProducedMana, SemanticProducedManaCombo, SemanticSelector,
28 SemanticSelectorAlternative, SemanticSelectorPart, SemanticTransform,
29};
30pub use cost::{CostToken, CostTokenKind};
31
32pub fn raw_has_key(raw: &str, key: &str) -> bool {
33 card_script::raw_has_key(raw, key)
34}
35
36pub fn raw_has_any(raw: &str, keys: &[&str]) -> bool {
37 card_script::raw_has_any(raw, keys)
38}
39
40pub fn raw_get<'a>(raw: &'a str, key: &str) -> Option<&'a str> {
41 card_script::raw_get(raw, key)
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct CompiledSelector {
53 pub alternatives: Vec<CompiledSelectorAlternative>,
54 pub ir: Selector,
55}
56
57impl CompiledSelector {
58 pub fn parse(raw: &str) -> Self {
59 crate::perf::increment(crate::perf::Metric::SelectorParses, 1);
60 match parse_semantic_param_value(keys::VALID, raw) {
61 SemanticParamValue::Selector(selector) | SemanticParamValue::Reference(selector) => {
62 compile_semantic_selector(&selector)
63 }
64 _ => CompiledSelector::from_alternatives(vec![CompiledSelectorAlternative {
65 raw: raw.trim().to_string(),
66 parts: vec![CompiledSelectorPart {
67 separator: None,
68 value: raw.trim().to_string(),
69 }],
70 }]),
71 }
72 }
73
74 pub fn from_alternatives(alternatives: Vec<CompiledSelectorAlternative>) -> Self {
75 let ir = lower_compiled_selector(&alternatives);
76 Self { alternatives, ir }
77 }
78
79 pub fn from_raw_alternative(raw: &str) -> Self {
80 Self::from_alternatives(vec![CompiledSelectorAlternative {
81 raw: raw.trim().to_string(),
82 parts: vec![CompiledSelectorPart {
83 separator: None,
84 value: raw.trim().to_string(),
85 }],
86 }])
87 }
88
89 pub fn is_any_of<const N: usize>(&self, values: [&str; N]) -> bool {
90 self.alternatives.len() == 1
91 && self.alternatives.first().is_some_and(|alternative| {
92 values
93 .iter()
94 .any(|value| alternative.raw.eq_ignore_ascii_case(value))
95 })
96 }
97
98 pub fn as_raw(&self) -> String {
99 self.alternatives
100 .iter()
101 .map(|alternative| alternative.raw.as_str())
102 .collect::<Vec<_>>()
103 .join(",")
104 }
105
106 pub fn raw_predicates(&self) -> impl Iterator<Item = &str> {
107 self.ir
108 .alternatives
109 .iter()
110 .flat_map(|alternative| &alternative.predicates)
111 .filter_map(|predicate| match predicate {
112 SelectorPredicate::Raw(raw) => Some(raw.as_str()),
113 _ => None,
114 })
115 }
116}
117
118pub fn cached_compiled_selector(raw: &str) -> CompiledSelector {
119 static CACHE: OnceLock<Mutex<BTreeMap<String, CompiledSelector>>> = OnceLock::new();
120 let key = raw.trim().to_string();
121 if let Some(selector) = common_compiled_selector(&key) {
122 return selector;
123 }
124 let cache = CACHE.get_or_init(|| Mutex::new(BTreeMap::new()));
125 if let Some(selector) = cache
126 .lock()
127 .expect("selector cache poisoned")
128 .get(&key)
129 .cloned()
130 {
131 return selector;
132 }
133 let selector = CompiledSelector::parse(&key);
134 cache
135 .lock()
136 .expect("selector cache poisoned")
137 .insert(key, selector.clone());
138 selector
139}
140
141fn common_compiled_selector(raw: &str) -> Option<CompiledSelector> {
142 match raw {
143 "Card.Self" => {
144 static SELECTOR: OnceLock<CompiledSelector> = OnceLock::new();
145 Some(
146 SELECTOR
147 .get_or_init(|| selector_from_parts("Card.Self", &["Card", "Self"]))
148 .clone(),
149 )
150 }
151 "Creature.Self" => {
152 static SELECTOR: OnceLock<CompiledSelector> = OnceLock::new();
153 Some(
154 SELECTOR
155 .get_or_init(|| selector_from_parts("Creature.Self", &["Creature", "Self"]))
156 .clone(),
157 )
158 }
159 "You" => {
160 static SELECTOR: OnceLock<CompiledSelector> = OnceLock::new();
161 Some(
162 SELECTOR
163 .get_or_init(|| selector_from_parts("You", &["You"]))
164 .clone(),
165 )
166 }
167 _ => None,
168 }
169}
170
171fn selector_from_parts(raw: &str, parts: &[&str]) -> CompiledSelector {
172 CompiledSelector::from_alternatives(vec![CompiledSelectorAlternative {
173 raw: raw.to_string(),
174 parts: parts
175 .iter()
176 .enumerate()
177 .map(|(index, value)| CompiledSelectorPart {
178 separator: (index > 0).then_some('.'),
179 value: (*value).to_string(),
180 })
181 .collect(),
182 }])
183}
184
185impl Serialize for CompiledSelector {
186 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
187 serializer.serialize_str(&self.as_raw())
188 }
189}
190
191impl<'de> Deserialize<'de> for CompiledSelector {
192 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
193 let raw = String::deserialize(deserializer)?;
194 Ok(Self::parse(&raw))
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct CompiledSelectorAlternative {
200 pub raw: String,
201 pub parts: Vec<CompiledSelectorPart>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct CompiledSelectorPart {
206 pub separator: Option<char>,
207 pub value: String,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Default)]
211pub struct Selector {
212 pub alternatives: Vec<SelectorAlt>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct SelectorAlt {
217 pub predicates: Vec<SelectorPredicate>,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
223pub enum SelectorPredicate {
224 Any,
225 Player,
226 CardType(CardSelectorType),
227 CardIdentity(CardIdentitySelector),
228 CardController(ControllerSelector),
229 CardOwner(ControllerSelector),
230 PlayerController(ControllerSelector),
231 CardSupertype(CardSupertypeSelector),
232 Tapped(bool),
233 StartedTurnTapped(bool),
234 CameUnderControlSinceLastUpkeep,
235 Zone(ZoneType),
236 RememberedCard,
237 TriggerRememberedCard,
238 EffectSource,
239 Commander,
240 Legendary,
241 Kicked,
242 Token(bool),
243 Color(CardColorSelector),
244 Multicolor,
245 Colorless,
246 SourceColor(CardColorSelector),
247 SourceColorless,
248 ChosenColorSource,
249 CardState(CardStateSelector),
250 Context(ContextPredicate),
251 Relation(RelationPredicate),
252 DamagedBy,
253 AttachedBy,
254 WasCast {
255 by_you: bool,
256 },
257 ChosenType,
258 Keyword {
259 name: String,
260 present: bool,
261 },
262 NumericComparison {
263 property: NumericSelectorProperty,
264 operator: SelectorCompareOperator,
265 value: SelectorNumericOperand,
266 },
267 NumericParity {
268 property: NumericSelectorProperty,
269 even: bool,
270 },
271 CounterComparison {
272 operator: SelectorCompareOperator,
273 value: SelectorNumericOperand,
274 counter_type: String,
275 },
276 Not(Box<SelectorPredicate>),
277 Raw(String),
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
282pub enum CardSelectorType {
283 Card,
284 Creature,
285 Land,
286 Instant,
287 Sorcery,
288 Artifact,
289 Enchantment,
290 Planeswalker,
291 Permanent,
292 Spell,
293 NonLand,
294 NonCreature,
295 Named(String),
296 Subtype(String),
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum CardSupertypeSelector {
301 Basic,
302 Snow,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum ControllerSelector {
307 You,
308 Opponent,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum CardIdentitySelector {
313 Self_,
314 Other,
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum CardColorSelector {
319 White,
320 Blue,
321 Black,
322 Red,
323 Green,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
328pub enum ContextPredicate {
329 Attacking(Option<TargetRef>),
330 Blocking(Option<TargetRef>),
331 BlockedByValidThisTurn(TargetRef),
332 BlockedByValidThisTurnType(CardSelectorType),
333 BlockedValidThisTurn(CardSelectorType),
334 BlockingValid(CardSelectorType),
335 Blocked,
336 AttackedThisTurn,
337 BlockingSource,
338 BlockedBySource,
339 WasCastFrom(CastOrigin),
340 EnteredThisTurnFrom(ZoneType),
341 EnteredUnder(TargetRef),
342 TopLibrary,
343 ExiledWithSource,
344 RememberedPlayerCtrl,
345 TargetedPlayerCtrl,
346 ActivePlayerCtrl,
347 DefenderCtrl,
348 EnchantedController,
349 ControlledBy(String),
350 NotDefinedTargeted,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub enum RelationPredicate {
355 SharesNameWith(TargetRef),
356 DoesNotShareNameWith(TargetRef),
357 SharesCardTypeWith(TargetRef),
358 SharesCreatureTypeWith(TargetRef),
359 SharesColorWith(TargetRef),
360 SharesManaValueWith(TargetRef),
361 AttachedTo(TargetRef),
362 AttachedToType(CardSelectorType),
363 OwnedBy(TargetRef),
364 OpponentOf(TargetRef),
365 IsTargeting(TargetRef),
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
369pub enum TargetRef {
370 Source,
371 Remembered,
372 RememberedLki,
373 Imprinted,
374 ChosenCard,
375 ChosenPlayer,
376 Targeted,
377 Player,
378 Opponent,
379 Battlefield,
380 OtherYourBattlefield,
381 YourGraveyard,
382 TriggeredTarget,
383 TriggeredPlayer,
384 TriggeredCard,
385 TriggeredCardController,
386 TriggeredDefendingPlayer,
387 TriggeredAttackedTarget,
388 Commander,
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395pub enum CastOrigin {
396 Hand,
397 YourHand,
398 YourHandByYou,
399 TheirHand,
400 Exile,
401 Graveyard,
402 YourGraveyard,
403 YourGraveyardByYou,
404 YourLibrary,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum CardStateSelector {
410 FaceDown,
411 Paired,
412 PairedWithSource,
413 Attached,
414 Equipped,
415 Enchanted,
416 HasCounters,
417 IsImprinted,
418 Chosen,
419 ChosenCard,
420 NamedCard,
421 ChosenColor,
422 EnteredThisTurn,
423 WasDealtDamageThisTurn,
424 Historic,
425 Modified,
426 Saddled,
427 MayPlaySource,
428 Suspended,
429 SingleTarget,
430 PromisedGift,
431 RingBearer,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436pub enum NumericSelectorProperty {
437 ManaValue,
438 Power,
439 Toughness,
440 TargetCount,
441 ManaSpent,
442}
443
444#[derive(Debug, Clone, PartialEq, Eq)]
445pub enum SelectorNumericOperand {
446 Literal(i32),
447 Symbol(String),
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451pub enum SelectorCompareOperator {
452 Eq,
453 Ne,
454 Lt,
455 Le,
456 Gt,
457 Ge,
458}
459
460#[derive(Debug, Clone, PartialEq, Eq)]
461enum CompiledParamValue {
462 Selector(CompiledSelector),
463 Reference(CompiledSelector),
464}
465
466#[derive(Debug, Clone, Default, Serialize, Deserialize)]
467#[serde(transparent)]
468pub struct Params(
469 BTreeMap<String, String>,
470 #[serde(skip)] BTreeMap<String, CompiledParamValue>,
471);
472
473impl Params {
474 pub fn from_raw(raw: &str) -> Self {
479 crate::perf::increment_params_parse();
480 let params = Self::from_parsed(&ParsedParams::parse(raw));
481 #[cfg(debug_assertions)]
482 debug_assert_eq!(params.inner(), &legacy_parse_params(raw));
483 params
484 }
485
486 pub fn from_parsed(parsed: &ParsedParams<'_>) -> Self {
491 let mut map = BTreeMap::new();
492 let mut compiled = BTreeMap::new();
493 for entry in parsed.entries() {
494 map.insert(entry.key.to_string(), entry.value.to_string());
495 if let Some(value) = compile_semantic_param_value(&entry.semantic().value) {
496 compiled.insert(entry.key.to_string(), value);
497 }
498 }
499 Params(map, compiled)
500 }
501
502 pub fn from_map(map: BTreeMap<String, String>) -> Self {
504 let compiled = compile_param_map(&map);
505 Params(map, compiled)
506 }
507
508 pub fn inner(&self) -> &BTreeMap<String, String> {
510 &self.0
511 }
512
513 pub fn into_inner(self) -> BTreeMap<String, String> {
515 self.0
516 }
517
518 pub fn get<K: AsRef<str>>(&self, key: K) -> Option<&str> {
523 crate::perf::increment_params_lookup();
524 self.0.get(key.as_ref()).map(|s| s.as_str())
525 }
526
527 pub fn get_or_default<'a, K: AsRef<str>>(&'a self, key: K, default: &'a str) -> &'a str {
530 crate::perf::increment_params_lookup();
531 self.0
532 .get(key.as_ref())
533 .map(|s| s.as_str())
534 .unwrap_or(default)
535 }
536
537 pub fn has<K: AsRef<str>>(&self, key: K) -> bool {
540 crate::perf::increment_params_lookup();
541 self.0.contains_key(key.as_ref())
542 }
543
544 pub fn put(&mut self, key: String, value: String) {
547 update_compiled_param(&mut self.1, &key, &value);
548 self.0.insert(key, value);
549 }
550
551 pub fn remove<K: AsRef<str>>(&mut self, key: K) -> Option<String> {
554 let key = key.as_ref();
555 self.1.remove(key);
556 self.0.remove(key)
557 }
558
559 pub fn is_empty(&self) -> bool {
561 self.0.is_empty()
562 }
563
564 pub fn contains_any_key(&self, keys: &[&str]) -> bool {
569 keys.iter().any(|key| self.0.contains_key(*key))
570 }
571
572 pub fn is_true<K: AsRef<str>>(&self, key: K) -> bool {
577 crate::perf::increment_params_lookup();
578 let key = key.as_ref();
579 let result = self
580 .0
581 .get(key)
582 .map(|value| match parse_semantic_param_value(key, value) {
583 SemanticParamValue::Boolean(value) => value,
584 _ => value.eq_ignore_ascii_case("True"),
585 })
586 .unwrap_or(false);
587 #[cfg(debug_assertions)]
588 debug_assert_eq!(
589 result,
590 self.0
591 .get(key)
592 .is_some_and(|value| value.eq_ignore_ascii_case("True")),
593 "semantic boolean param {} diverged from string params",
594 key
595 );
596 result
597 }
598
599 pub fn as_i32<K: AsRef<str>>(&self, key: K) -> Option<i32> {
601 crate::perf::increment_params_lookup();
602 let key = key.as_ref();
603 let result = self.0.get(key).and_then(|value| semantic_i32(key, value));
604 #[cfg(debug_assertions)]
605 debug_assert_eq!(
606 result,
607 self.0.get(key).and_then(|value| value.trim().parse().ok()),
608 "semantic i32 param {} diverged from string params",
609 key
610 );
611 result
612 }
613
614 pub fn as_usize<K: AsRef<str>>(&self, key: K) -> Option<usize> {
616 crate::perf::increment_params_lookup();
617 let key = key.as_ref();
618 let result = self
619 .0
620 .get(key)
621 .and_then(|value| semantic_i32(key, value))
622 .and_then(|value| usize::try_from(value).ok());
623 #[cfg(debug_assertions)]
624 debug_assert_eq!(
625 result,
626 self.0.get(key).and_then(|value| value.trim().parse().ok()),
627 "semantic usize param {} diverged from string params",
628 key
629 );
630 result
631 }
632
633 pub fn zone_type<K: AsRef<str>>(&self, key: K) -> Option<ZoneType> {
635 crate::perf::increment_params_lookup();
636 let key = key.as_ref();
637 let result = self
638 .0
639 .get(key)
640 .and_then(|value| semantic_zone_type(key, value));
641 #[cfg(debug_assertions)]
642 debug_assert_eq!(
643 result,
644 self.0.get(key).and_then(|value| legacy_zone_type(value)),
645 "semantic zone param {} diverged from string params",
646 key
647 );
648 result
649 }
650
651 pub fn zone_types<K: AsRef<str>>(&self, key: K) -> Vec<ZoneType> {
653 crate::perf::increment_params_lookup();
654 let key = key.as_ref();
655 let result = self
656 .0
657 .get(key)
658 .map(|value| semantic_zone_types(key, value))
659 .unwrap_or_default();
660 #[cfg(debug_assertions)]
661 debug_assert_eq!(
662 result,
663 self.0
664 .get(key)
665 .map(|value| legacy_zone_types(value))
666 .unwrap_or_default(),
667 "semantic zone-list param {} diverged from string params",
668 key
669 );
670 result
671 }
672
673 pub fn selector_value<K: AsRef<str>>(&self, key: K) -> Option<&str> {
677 crate::perf::increment_params_lookup();
678 let key = key.as_ref();
679 let value = self.0.get(key).map(String::as_str)?;
680 #[cfg(debug_assertions)]
681 debug_assert!(
682 matches_selector_semantics(key, value),
683 "semantic selector param {} classified unexpectedly: {:?}",
684 key,
685 parse_semantic_param_value(key, value)
686 );
687 Some(value)
688 }
689
690 pub fn selector<K: AsRef<str>>(&self, key: K) -> Option<&CompiledSelector> {
692 crate::perf::increment_params_lookup();
693 let key = key.as_ref();
694 self.selector_untracked(key)
695 }
696
697 pub fn selector_untracked<K: AsRef<str>>(&self, key: K) -> Option<&CompiledSelector> {
698 let key = key.as_ref();
699 match self.1.get(key) {
700 Some(CompiledParamValue::Selector(selector))
701 | Some(CompiledParamValue::Reference(selector)) => Some(selector),
702 None => None,
703 }
704 }
705
706 pub fn selector_cloned<K: AsRef<str>>(&self, key: K) -> Option<CompiledSelector> {
713 crate::perf::increment_params_lookup();
714 let key = key.as_ref();
715 match self.1.get(key) {
716 Some(CompiledParamValue::Selector(selector))
717 | Some(CompiledParamValue::Reference(selector)) => Some(selector.clone()),
718 None => self.0.get(key).map(|value| cached_compiled_selector(value)),
719 }
720 }
721
722 pub fn selector_cloned_any(&self, keys: &[&str]) -> Option<CompiledSelector> {
723 keys.iter().find_map(|key| self.selector_cloned(*key))
724 }
725
726 pub fn reference_value<K: AsRef<str>>(&self, key: K) -> Option<&str> {
729 crate::perf::increment_params_lookup();
730 let key = key.as_ref();
731 let value = self.0.get(key).map(String::as_str)?;
732 #[cfg(debug_assertions)]
733 debug_assert!(
734 matches_reference_semantics(key, value),
735 "semantic reference param {} classified unexpectedly: {:?}",
736 key,
737 parse_semantic_param_value(key, value)
738 );
739 Some(value)
740 }
741
742 pub fn reference<K: AsRef<str>>(&self, key: K) -> Option<&CompiledSelector> {
744 crate::perf::increment_params_lookup();
745 let key = key.as_ref();
746 match self.1.get(key) {
747 Some(CompiledParamValue::Reference(selector))
748 | Some(CompiledParamValue::Selector(selector)) => Some(selector),
749 None => None,
750 }
751 }
752
753 pub fn get_cloned(&self, key: &str) -> Option<String> {
755 self.0.get(key).cloned()
756 }
757
758 pub fn split_param_list<K: AsRef<str>>(&self, key: K, delimiter: &str) -> Vec<String> {
760 split_param_list_value(self.get(key), delimiter)
761 }
762
763 pub fn semantic_value<K: AsRef<str>>(&self, key: K) -> Option<SemanticParamValue<'_>> {
768 crate::perf::increment_params_lookup();
769 let key = key.as_ref();
770 self.0
771 .get(key)
772 .map(|value| parse_semantic_param_value(key, value))
773 }
774
775 pub fn require(&self, key: &str, context: &str) -> Option<&str> {
782 match self.get(key) {
783 Some(v) => Some(v),
784 None => {
785 eprintln!("[parse] missing required param '{}' in {}", key, context);
786 None
787 }
788 }
789 }
790
791 pub fn require_cloned(&self, key: &str, context: &str) -> Option<String> {
793 self.require(key, context).map(|s| s.to_string())
794 }
795
796 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
800 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
801 }
802}
803
804pub fn split_param_list_value(value: Option<&str>, delimiter: &str) -> Vec<String> {
805 value
806 .map(|value| {
807 value
808 .split(delimiter)
809 .map(str::trim)
810 .filter(|part| !part.is_empty())
811 .map(str::to_owned)
812 .collect()
813 })
814 .unwrap_or_default()
815}
816
817fn semantic_i32(key: &str, value: &str) -> Option<i32> {
818 match parse_semantic_param_value(key, value) {
819 SemanticParamValue::Integer(value) => Some(value),
820 SemanticParamValue::Amount(SemanticAmount::Literal(value)) => Some(value),
821 _ => value.trim().parse().ok(),
822 }
823}
824
825fn semantic_zone_type(key: &str, value: &str) -> Option<ZoneType> {
826 match parse_semantic_param_value(key, value) {
827 SemanticParamValue::ZoneList(zones) if zones.len() == 1 => {
828 zones.first().and_then(|zone| legacy_zone_type(zone))
829 }
830 SemanticParamValue::ZoneList(_) => None,
831 _ => legacy_zone_type(value),
832 }
833}
834
835fn semantic_zone_types(key: &str, value: &str) -> Vec<ZoneType> {
836 match parse_semantic_param_value(key, value) {
837 SemanticParamValue::ZoneList(zones) => {
838 zones.into_iter().filter_map(legacy_zone_type).collect()
839 }
840 _ => legacy_zone_types(value),
841 }
842}
843
844fn legacy_zone_type(value: &str) -> Option<ZoneType> {
845 let value = value.trim();
846 if value.eq_ignore_ascii_case("Deck") {
847 Some(ZoneType::Library)
848 } else {
849 ZoneType::from_str_compat(value)
850 }
851}
852
853fn legacy_zone_types(value: &str) -> Vec<ZoneType> {
854 value
855 .split([',', ' '])
856 .map(str::trim)
857 .filter(|part| !part.is_empty())
858 .filter_map(legacy_zone_type)
859 .collect()
860}
861
862fn compile_param_map(map: &BTreeMap<String, String>) -> BTreeMap<String, CompiledParamValue> {
863 map.iter()
864 .filter_map(|(key, value)| {
865 compile_semantic_param_value(&parse_semantic_param_value(key, value))
866 .map(|compiled| (key.clone(), compiled))
867 })
868 .collect()
869}
870
871fn update_compiled_param(
872 compiled: &mut BTreeMap<String, CompiledParamValue>,
873 key: &str,
874 value: &str,
875) {
876 match compile_semantic_param_value(&parse_semantic_param_value(key, value)) {
877 Some(value) => {
878 compiled.insert(key.to_string(), value);
879 }
880 None => {
881 compiled.remove(key);
882 }
883 }
884}
885
886fn compile_semantic_param_value(value: &SemanticParamValue<'_>) -> Option<CompiledParamValue> {
887 match value {
888 SemanticParamValue::Selector(selector) => Some(CompiledParamValue::Selector(
889 compile_semantic_selector(selector),
890 )),
891 SemanticParamValue::Reference(selector) => Some(CompiledParamValue::Reference(
892 compile_semantic_selector(selector),
893 )),
894 _ => None,
895 }
896}
897
898fn compile_semantic_selector(selector: &SemanticSelector<'_>) -> CompiledSelector {
899 let alternatives = selector
900 .alternatives
901 .iter()
902 .map(|alternative| CompiledSelectorAlternative {
903 raw: alternative.raw.to_string(),
904 parts: alternative
905 .parts
906 .iter()
907 .map(|part| CompiledSelectorPart {
908 separator: part.separator,
909 value: part.value.to_string(),
910 })
911 .collect(),
912 })
913 .collect::<Vec<_>>();
914 let ir = lower_compiled_selector(&alternatives);
915 CompiledSelector { alternatives, ir }
916}
917
918fn lower_compiled_selector(alternatives: &[CompiledSelectorAlternative]) -> Selector {
919 Selector {
920 alternatives: alternatives
921 .iter()
922 .map(|alternative| {
923 let mut predicates: Vec<_> = alternative
924 .parts
925 .iter()
926 .enumerate()
927 .map(|(idx, part)| lower_selector_part(&part.value, idx == 0))
928 .collect();
929 predicates.sort_by_key(selector_predicate_order);
930 SelectorAlt { predicates }
931 })
932 .collect(),
933 }
934}
935
936fn selector_predicate_order(predicate: &SelectorPredicate) -> u8 {
937 match predicate {
938 SelectorPredicate::Any
939 | SelectorPredicate::CardType(_)
940 | SelectorPredicate::CardSupertype(_)
941 | SelectorPredicate::Zone(_)
942 | SelectorPredicate::CardController(_)
943 | SelectorPredicate::CardOwner(_)
944 | SelectorPredicate::Tapped(_)
945 | SelectorPredicate::StartedTurnTapped(_)
946 | SelectorPredicate::CameUnderControlSinceLastUpkeep
947 | SelectorPredicate::Token(_)
948 | SelectorPredicate::Color(_)
949 | SelectorPredicate::Multicolor
950 | SelectorPredicate::Colorless
951 | SelectorPredicate::Commander
952 | SelectorPredicate::Legendary
953 | SelectorPredicate::Kicked => 0,
954 SelectorPredicate::CardIdentity(_) | SelectorPredicate::PlayerController(_) => 1,
955 SelectorPredicate::NumericComparison { .. }
956 | SelectorPredicate::NumericParity { .. }
957 | SelectorPredicate::CounterComparison { .. }
958 | SelectorPredicate::Keyword { .. }
959 | SelectorPredicate::CardState(_)
960 | SelectorPredicate::ChosenType
961 | SelectorPredicate::WasCast { .. }
962 | SelectorPredicate::SourceColor(_)
963 | SelectorPredicate::SourceColorless
964 | SelectorPredicate::ChosenColorSource => 2,
965 SelectorPredicate::Context(_) => 3,
966 SelectorPredicate::Relation(_) => 4,
967 SelectorPredicate::RememberedCard
968 | SelectorPredicate::TriggerRememberedCard
969 | SelectorPredicate::EffectSource
970 | SelectorPredicate::DamagedBy
971 | SelectorPredicate::AttachedBy => 5,
972 SelectorPredicate::Not(inner) => selector_predicate_order(inner).saturating_add(1),
973 SelectorPredicate::Player => 6,
974 SelectorPredicate::Raw(_) => 7,
975 }
976}
977
978fn lower_selector_part(value: &str, is_first_part: bool) -> SelectorPredicate {
979 let normalized = value.trim();
980 if normalized.is_empty() {
981 return SelectorPredicate::Raw(String::new());
982 }
983 let lower = normalized.to_ascii_lowercase();
984 if is_first_part {
985 return match lower.as_str() {
986 "any" => SelectorPredicate::Any,
987 "card" => SelectorPredicate::CardType(CardSelectorType::Card),
988 "creature" => SelectorPredicate::CardType(CardSelectorType::Creature),
989 "land" => SelectorPredicate::CardType(CardSelectorType::Land),
990 "instant" => SelectorPredicate::CardType(CardSelectorType::Instant),
991 "sorcery" => SelectorPredicate::CardType(CardSelectorType::Sorcery),
992 "artifact" => SelectorPredicate::CardType(CardSelectorType::Artifact),
993 "enchantment" => SelectorPredicate::CardType(CardSelectorType::Enchantment),
994 "planeswalker" => SelectorPredicate::CardType(CardSelectorType::Planeswalker),
995 "permanent" => SelectorPredicate::CardType(CardSelectorType::Permanent),
996 "spell" => SelectorPredicate::CardType(CardSelectorType::Spell),
997 "nonland" => SelectorPredicate::CardType(CardSelectorType::NonLand),
998 "noncreature" => SelectorPredicate::CardType(CardSelectorType::NonCreature),
999 "player" | "each" | "player.ingame" => SelectorPredicate::Player,
1000 "you" | "youctrl" => SelectorPredicate::PlayerController(ControllerSelector::You),
1001 "opponent" | "oppctrl" | "opponentctrl" => {
1002 SelectorPredicate::PlayerController(ControllerSelector::Opponent)
1003 }
1004 named if named.starts_with("named") => SelectorPredicate::CardType(
1005 CardSelectorType::Named(normalized[5..].trim().to_string()),
1006 ),
1007 _ => SelectorPredicate::CardType(CardSelectorType::Subtype(normalized.to_string())),
1008 };
1009 }
1010
1011 if let Some(stripped) = normalized.strip_prefix('!') {
1012 let predicate = lower_selector_part(stripped, false);
1013 return SelectorPredicate::Not(Box::new(predicate));
1014 }
1015
1016 match lower.as_str() {
1017 "self" | "strictlyself" => SelectorPredicate::CardIdentity(CardIdentitySelector::Self_),
1018 "other" | "strictlyother" => SelectorPredicate::CardIdentity(CardIdentitySelector::Other),
1019 "you" | "youctrl" | "youcontrol" => {
1020 SelectorPredicate::CardController(ControllerSelector::You)
1021 }
1022 "opponent" | "oppctrl" | "opponentctrl" => {
1023 SelectorPredicate::CardController(ControllerSelector::Opponent)
1024 }
1025 "chosenctrl" => SelectorPredicate::Raw(normalized.to_string()),
1026 "youown" => SelectorPredicate::CardOwner(ControllerSelector::You),
1027 "oppown" | "opponentown" => SelectorPredicate::CardOwner(ControllerSelector::Opponent),
1028 "targetedplayerown" | "targetedown" | "targetedowner" => {
1031 SelectorPredicate::Raw(normalized.to_string())
1032 }
1033 "youdontctrl" => SelectorPredicate::Not(Box::new(SelectorPredicate::CardController(
1034 ControllerSelector::You,
1035 ))),
1036 "youdontown" => SelectorPredicate::Not(Box::new(SelectorPredicate::CardOwner(
1037 ControllerSelector::You,
1038 ))),
1039 "tapped" => SelectorPredicate::Tapped(true),
1040 "untapped" => SelectorPredicate::Tapped(false),
1041 "startedtheturntapped" => SelectorPredicate::StartedTurnTapped(true),
1042 "startedtheturnuntapped" => SelectorPredicate::StartedTurnTapped(false),
1043 "cameundercontrolsincelastupkeep" => SelectorPredicate::CameUnderControlSinceLastUpkeep,
1044 "inzonebattlefield" => SelectorPredicate::Zone(ZoneType::Battlefield),
1045 "inzonegraveyard" => SelectorPredicate::Zone(ZoneType::Graveyard),
1046 "inzonehand" => SelectorPredicate::Zone(ZoneType::Hand),
1047 "inzoneexile" => SelectorPredicate::Zone(ZoneType::Exile),
1048 "inzonestack" => SelectorPredicate::Zone(ZoneType::Stack),
1049 "isremembered" => SelectorPredicate::RememberedCard,
1050 "istriggerremembered" => SelectorPredicate::TriggerRememberedCard,
1051 "effectsource" => SelectorPredicate::EffectSource,
1052 "iscommander" => SelectorPredicate::Commander,
1053 "legendary" => SelectorPredicate::Legendary,
1054 "basic" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Basic),
1055 "snow" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Snow),
1056 "kicked" => SelectorPredicate::Kicked,
1057 "token" => SelectorPredicate::Token(true),
1058 "nontoken" => SelectorPredicate::Token(false),
1059 "creature" => SelectorPredicate::CardType(CardSelectorType::Creature),
1060 "land" => SelectorPredicate::CardType(CardSelectorType::Land),
1061 "instant" => SelectorPredicate::CardType(CardSelectorType::Instant),
1062 "sorcery" => SelectorPredicate::CardType(CardSelectorType::Sorcery),
1063 "artifact" => SelectorPredicate::CardType(CardSelectorType::Artifact),
1064 "enchantment" => SelectorPredicate::CardType(CardSelectorType::Enchantment),
1065 "planeswalker" => SelectorPredicate::CardType(CardSelectorType::Planeswalker),
1066 "permanent" => SelectorPredicate::CardType(CardSelectorType::Permanent),
1067 "noncreature" => SelectorPredicate::CardType(CardSelectorType::NonCreature),
1068 "nonland" => SelectorPredicate::CardType(CardSelectorType::NonLand),
1069 "time lord" => {
1070 SelectorPredicate::CardType(CardSelectorType::Subtype("Time Lord".to_string()))
1071 }
1072 "white" => SelectorPredicate::Color(CardColorSelector::White),
1073 "blue" => SelectorPredicate::Color(CardColorSelector::Blue),
1074 "black" => SelectorPredicate::Color(CardColorSelector::Black),
1075 "red" => SelectorPredicate::Color(CardColorSelector::Red),
1076 "green" => SelectorPredicate::Color(CardColorSelector::Green),
1077 "multicolor" => SelectorPredicate::Multicolor,
1078 "colorless" => SelectorPredicate::Colorless,
1079 "whitesource" => SelectorPredicate::SourceColor(CardColorSelector::White),
1080 "bluesource" => SelectorPredicate::SourceColor(CardColorSelector::Blue),
1081 "blacksource" => SelectorPredicate::SourceColor(CardColorSelector::Black),
1082 "redsource" => SelectorPredicate::SourceColor(CardColorSelector::Red),
1083 "greensource" => SelectorPredicate::SourceColor(CardColorSelector::Green),
1084 "colorlesssource" => SelectorPredicate::SourceColorless,
1085 "chosencolorsource" => SelectorPredicate::ChosenColorSource,
1086 "attacking" => SelectorPredicate::Context(ContextPredicate::Attacking(None)),
1087 "attackingyou" => {
1088 SelectorPredicate::Context(ContextPredicate::Attacking(Some(TargetRef::Source)))
1089 }
1090 "blocking" => SelectorPredicate::Context(ContextPredicate::Blocking(None)),
1091 "blocked" => SelectorPredicate::Context(ContextPredicate::Blocked),
1092 "attackedthisturn" => SelectorPredicate::Context(ContextPredicate::AttackedThisTurn),
1093 "blockingsource" => SelectorPredicate::Context(ContextPredicate::BlockingSource),
1094 "blockedbysource" => SelectorPredicate::Context(ContextPredicate::BlockedBySource),
1095 "samename" => {
1096 SelectorPredicate::Relation(RelationPredicate::SharesNameWith(TargetRef::Source))
1097 }
1098 "damagedby" => SelectorPredicate::DamagedBy,
1099 "equippedby" | "enchantedby" | "attachedby" => SelectorPredicate::AttachedBy,
1100 "facedown" => SelectorPredicate::CardState(CardStateSelector::FaceDown),
1101 "paired" => SelectorPredicate::CardState(CardStateSelector::Paired),
1102 "pairedwith" => SelectorPredicate::CardState(CardStateSelector::PairedWithSource),
1103 "attached" => SelectorPredicate::CardState(CardStateSelector::Attached),
1104 "equipped" => SelectorPredicate::CardState(CardStateSelector::Equipped),
1105 "enchanted" => SelectorPredicate::CardState(CardStateSelector::Enchanted),
1106 "hascounters" => SelectorPredicate::CardState(CardStateSelector::HasCounters),
1107 "isimprinted" => SelectorPredicate::CardState(CardStateSelector::IsImprinted),
1108 "chosen" => SelectorPredicate::CardState(CardStateSelector::Chosen),
1109 "chosencard" | "chosencardstrict" => {
1110 SelectorPredicate::CardState(CardStateSelector::ChosenCard)
1111 }
1112 "namedcard" => SelectorPredicate::CardState(CardStateSelector::NamedCard),
1113 "chosencolor" => SelectorPredicate::CardState(CardStateSelector::ChosenColor),
1114 "thisturnentered" => SelectorPredicate::CardState(CardStateSelector::EnteredThisTurn),
1115 "wasdealtdamagethisturn" => {
1116 SelectorPredicate::CardState(CardStateSelector::WasDealtDamageThisTurn)
1117 }
1118 "historic" => SelectorPredicate::CardState(CardStateSelector::Historic),
1119 "modified" => SelectorPredicate::CardState(CardStateSelector::Modified),
1120 "issaddled" => SelectorPredicate::CardState(CardStateSelector::Saddled),
1121 "mayplaysource" => SelectorPredicate::CardState(CardStateSelector::MayPlaySource),
1122 "exiledwithsource" => SelectorPredicate::Context(ContextPredicate::ExiledWithSource),
1123 "toplibrary" => SelectorPredicate::Context(ContextPredicate::TopLibrary),
1124 "suspended" => SelectorPredicate::CardState(CardStateSelector::Suspended),
1125 "singletarget" => SelectorPredicate::CardState(CardStateSelector::SingleTarget),
1126 "promisedgift" => SelectorPredicate::CardState(CardStateSelector::PromisedGift),
1127 "isringbearer" => SelectorPredicate::CardState(CardStateSelector::RingBearer),
1128 "wascast" => SelectorPredicate::WasCast { by_you: false },
1129 "wascastbyyou" => SelectorPredicate::WasCast { by_you: true },
1130 named if named.starts_with("named") => {
1131 SelectorPredicate::CardType(CardSelectorType::Named(normalized[5..].trim().to_string()))
1132 }
1133 cast_origin if cast_origin.starts_with("wascastfrom") => {
1134 lower_cast_origin_predicate(normalized)
1135 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1136 }
1137 "chosentype" => SelectorPredicate::ChosenType,
1138 "rememberedplayerctrl" => {
1139 SelectorPredicate::Context(ContextPredicate::RememberedPlayerCtrl)
1140 }
1141 "targetedplayerctrl" => SelectorPredicate::Context(ContextPredicate::TargetedPlayerCtrl),
1142 "activeplayerctrl" => SelectorPredicate::Context(ContextPredicate::ActivePlayerCtrl),
1143 "defenderctrl" => SelectorPredicate::Context(ContextPredicate::DefenderCtrl),
1144 "enchantedcontroller" => SelectorPredicate::Context(ContextPredicate::EnchantedController),
1145 "notdefinedtargeted" => SelectorPredicate::Context(ContextPredicate::NotDefinedTargeted),
1146 controlled if controlled.starts_with("controlledby ") => SelectorPredicate::Context(
1147 ContextPredicate::ControlledBy(normalized["ControlledBy ".len()..].trim().to_string()),
1148 ),
1149 shares if shares.starts_with("sharesnamewith") => {
1150 lower_relation_target_ref(normalized["sharesNameWith".len()..].trim())
1151 .map(|target| {
1152 SelectorPredicate::Relation(RelationPredicate::SharesNameWith(target))
1153 })
1154 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1155 }
1156 shares if shares.starts_with("doesnotsharenamewith") => {
1157 lower_relation_target_ref(normalized["doesNotShareNameWith".len()..].trim())
1158 .map(|target| {
1159 SelectorPredicate::Relation(RelationPredicate::DoesNotShareNameWith(target))
1160 })
1161 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1162 }
1163 shares if shares.starts_with("sharescardtypewith") => {
1164 lower_relation_target_ref(normalized["sharesCardTypeWith".len()..].trim())
1165 .map(|target| {
1166 SelectorPredicate::Relation(RelationPredicate::SharesCardTypeWith(target))
1167 })
1168 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1169 }
1170 shares if shares.starts_with("sharescolorwith") => {
1171 lower_relation_target_ref(normalized["SharesColorWith".len()..].trim())
1172 .map(|target| {
1173 SelectorPredicate::Relation(RelationPredicate::SharesColorWith(target))
1174 })
1175 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1176 }
1177 shares if shares.starts_with("sharescmcwith") => {
1178 lower_relation_target_ref(normalized["SharesCMCWith".len()..].trim())
1179 .map(|target| {
1180 SelectorPredicate::Relation(RelationPredicate::SharesManaValueWith(target))
1181 })
1182 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1183 }
1184 shares if shares.starts_with("sharescreaturetypewith") => {
1185 lower_relation_target_ref(normalized["sharesCreatureTypeWith".len()..].trim())
1186 .map(|target| {
1187 SelectorPredicate::Relation(RelationPredicate::SharesCreatureTypeWith(target))
1188 })
1189 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1190 }
1191 attacking if attacking.starts_with("attacking ") => {
1192 lower_relation_target_ref(normalized["attacking ".len()..].trim())
1193 .map(|target| SelectorPredicate::Context(ContextPredicate::Attacking(Some(target))))
1194 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1195 }
1196 blocked_by if blocked_by.starts_with("blockedbyvalidthisturn ") => {
1197 lower_blocked_by_valid_this_turn(normalized["blockedByValidThisTurn ".len()..].trim())
1198 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1199 }
1200 blocked if blocked.starts_with("blockedvalidthisturn ") => {
1201 lower_card_selector_type(normalized["blockedValidThisTurn ".len()..].trim())
1202 .map(|card_type| {
1203 SelectorPredicate::Context(ContextPredicate::BlockedValidThisTurn(card_type))
1204 })
1205 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1206 }
1207 blocking_valid if blocking_valid.starts_with("blockingvalid ") => {
1208 lower_card_selector_type(normalized["blockingValid ".len()..].trim())
1209 .map(|card_type| {
1210 SelectorPredicate::Context(ContextPredicate::BlockingValid(card_type))
1211 })
1212 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1213 }
1214 blocking if blocking.starts_with("blocking ") => {
1215 lower_relation_target_ref(normalized["blocking ".len()..].trim())
1216 .map(|target| SelectorPredicate::Context(ContextPredicate::Blocking(Some(target))))
1217 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1218 }
1219 attached if attached.starts_with("attachedto ") => {
1220 lower_attached_to_relation(normalized["AttachedTo ".len()..].trim())
1221 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1222 }
1223 owned if owned.starts_with("ownedby ") => {
1224 lower_relation_target_ref(normalized["OwnedBy ".len()..].trim())
1225 .map(|target| SelectorPredicate::Relation(RelationPredicate::OwnedBy(target)))
1226 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1227 }
1228 opponent if opponent.starts_with("opponentof ") => {
1229 lower_relation_target_ref(normalized["OpponentOf ".len()..].trim())
1230 .map(|target| SelectorPredicate::Relation(RelationPredicate::OpponentOf(target)))
1231 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1232 }
1233 targeting if targeting.starts_with("istargeting ") => {
1234 lower_relation_target_ref(normalized["IsTargeting ".len()..].trim())
1235 .map(|target| SelectorPredicate::Relation(RelationPredicate::IsTargeting(target)))
1236 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1237 }
1238 entered if entered.starts_with("thisturnenteredfrom_") => {
1239 lower_entered_from_zone(normalized["ThisTurnEnteredFrom_".len()..].trim())
1240 .map(|zone| SelectorPredicate::Context(ContextPredicate::EnteredThisTurnFrom(zone)))
1241 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1242 }
1243 entered if entered.starts_with("enteredunder ") => {
1244 lower_relation_target_ref(normalized["EnteredUnder ".len()..].trim())
1245 .map(|target| SelectorPredicate::Context(ContextPredicate::EnteredUnder(target)))
1246 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1247 }
1248 _ => lower_non_predicate(normalized).unwrap_or_else(|| {
1249 lower_keyword_predicate(normalized).unwrap_or_else(|| {
1250 lower_selector_comparison(normalized).unwrap_or_else(|| {
1251 lower_counter_comparison(normalized).unwrap_or_else(|| {
1252 lower_subtype_predicate(normalized)
1253 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1254 })
1255 })
1256 })
1257 }),
1258 }
1259}
1260
1261fn lower_relation_target_ref(value: &str) -> Option<TargetRef> {
1262 if value.is_empty()
1263 || value.eq_ignore_ascii_case("Self")
1264 || value.eq_ignore_ascii_case("Source")
1265 || value.eq_ignore_ascii_case("You")
1266 || value.eq_ignore_ascii_case("YouCtrl")
1267 {
1268 Some(TargetRef::Source)
1269 } else if value.eq_ignore_ascii_case("Remembered") {
1270 Some(TargetRef::Remembered)
1271 } else if value.eq_ignore_ascii_case("RememberedLKI") {
1272 Some(TargetRef::RememberedLki)
1273 } else if value.eq_ignore_ascii_case("Imprinted") {
1274 Some(TargetRef::Imprinted)
1275 } else if value.eq_ignore_ascii_case("ChosenCard") {
1276 Some(TargetRef::ChosenCard)
1277 } else if value.eq_ignore_ascii_case("ChosenPlayer") {
1278 Some(TargetRef::ChosenPlayer)
1279 } else if value.eq_ignore_ascii_case("Targeted")
1280 || value.eq_ignore_ascii_case("TargetedPlayer")
1281 || value.eq_ignore_ascii_case("TargetedController")
1282 {
1283 Some(TargetRef::Targeted)
1284 } else if value.eq_ignore_ascii_case("Player") {
1285 Some(TargetRef::Player)
1286 } else if value.eq_ignore_ascii_case("Opponent") {
1287 Some(TargetRef::Opponent)
1288 } else if value.eq_ignore_ascii_case("Battlefield") {
1289 Some(TargetRef::Battlefield)
1290 } else if value.eq_ignore_ascii_case("OtherYourBattlefield") {
1291 Some(TargetRef::OtherYourBattlefield)
1292 } else if value.eq_ignore_ascii_case("YourGraveyard") {
1293 Some(TargetRef::YourGraveyard)
1294 } else if value.eq_ignore_ascii_case("TriggeredTarget") {
1295 Some(TargetRef::TriggeredTarget)
1296 } else if value.eq_ignore_ascii_case("TriggeredPlayer") {
1297 Some(TargetRef::TriggeredPlayer)
1298 } else if value.eq_ignore_ascii_case("TriggeredCard") {
1299 Some(TargetRef::TriggeredCard)
1300 } else if value.eq_ignore_ascii_case("TriggeredCardController") {
1301 Some(TargetRef::TriggeredCardController)
1302 } else if value.eq_ignore_ascii_case("TriggeredDefendingPlayer") {
1303 Some(TargetRef::TriggeredDefendingPlayer)
1304 } else if value.eq_ignore_ascii_case("TriggeredAttackedTarget") {
1305 Some(TargetRef::TriggeredAttackedTarget)
1306 } else if value.eq_ignore_ascii_case("Commander") {
1307 Some(TargetRef::Commander)
1308 } else {
1309 None
1310 }
1311}
1312
1313fn lower_blocked_by_valid_this_turn(value: &str) -> Option<SelectorPredicate> {
1314 if let Some(target) = lower_relation_target_ref(value) {
1315 return Some(SelectorPredicate::Context(
1316 ContextPredicate::BlockedByValidThisTurn(target),
1317 ));
1318 }
1319 lower_card_selector_type(value).map(|card_type| {
1320 SelectorPredicate::Context(ContextPredicate::BlockedByValidThisTurnType(card_type))
1321 })
1322}
1323
1324fn lower_card_selector_type(value: &str) -> Option<CardSelectorType> {
1325 match value.to_ascii_lowercase().as_str() {
1326 "card" => Some(CardSelectorType::Card),
1327 "creature" => Some(CardSelectorType::Creature),
1328 "land" => Some(CardSelectorType::Land),
1329 "artifact" => Some(CardSelectorType::Artifact),
1330 "enchantment" => Some(CardSelectorType::Enchantment),
1331 "planeswalker" => Some(CardSelectorType::Planeswalker),
1332 "permanent" => Some(CardSelectorType::Permanent),
1333 "nonland" => Some(CardSelectorType::NonLand),
1334 "noncreature" => Some(CardSelectorType::NonCreature),
1335 _ if value
1336 .chars()
1337 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '\'') =>
1338 {
1339 Some(CardSelectorType::Subtype(value.to_string()))
1340 }
1341 _ => None,
1342 }
1343}
1344
1345fn lower_entered_from_zone(value: &str) -> Option<ZoneType> {
1346 match value.to_ascii_lowercase().as_str() {
1347 "battlefield" => Some(ZoneType::Battlefield),
1348 "graveyard" => Some(ZoneType::Graveyard),
1349 "library" => Some(ZoneType::Library),
1350 "hand" => Some(ZoneType::Hand),
1351 "exile" => Some(ZoneType::Exile),
1352 _ => None,
1353 }
1354}
1355
1356fn lower_attached_to_relation(value: &str) -> Option<SelectorPredicate> {
1357 if let Some(target) = lower_relation_target_ref(value) {
1358 return Some(SelectorPredicate::Relation(RelationPredicate::AttachedTo(
1359 target,
1360 )));
1361 }
1362 let card_type = match value.to_ascii_lowercase().as_str() {
1363 "card" => CardSelectorType::Card,
1364 "creature" => CardSelectorType::Creature,
1365 "land" => CardSelectorType::Land,
1366 "artifact" => CardSelectorType::Artifact,
1367 "enchantment" => CardSelectorType::Enchantment,
1368 "permanent" => CardSelectorType::Permanent,
1369 _ => return None,
1370 };
1371 Some(SelectorPredicate::Relation(
1372 RelationPredicate::AttachedToType(card_type),
1373 ))
1374}
1375
1376fn lower_cast_origin_predicate(value: &str) -> Option<SelectorPredicate> {
1377 let lower = value.to_ascii_lowercase();
1378 let origin = match lower.as_str() {
1379 "wascastfromhand" => CastOrigin::Hand,
1380 "wascastfromyourhand" => CastOrigin::YourHand,
1381 "wascastfromyourhandbyyou" => CastOrigin::YourHandByYou,
1382 "wascastfromtheirhand" => CastOrigin::TheirHand,
1383 "wascastfromexile" => CastOrigin::Exile,
1384 "wascastfromgraveyard" => CastOrigin::Graveyard,
1385 "wascastfromyourgraveyard" => CastOrigin::YourGraveyard,
1386 "wascastfromyourgraveyardbyyou" => CastOrigin::YourGraveyardByYou,
1387 "wascastfromyourlibrary" => CastOrigin::YourLibrary,
1388 _ => return None,
1389 };
1390 Some(SelectorPredicate::Context(ContextPredicate::WasCastFrom(
1391 origin,
1392 )))
1393}
1394
1395fn lower_non_predicate(value: &str) -> Option<SelectorPredicate> {
1396 let lower = value.to_ascii_lowercase();
1397 let rest = lower.strip_prefix("non")?;
1398 if rest.is_empty() {
1399 return None;
1400 }
1401 let positive = match rest {
1402 "white" => SelectorPredicate::Color(CardColorSelector::White),
1403 "blue" => SelectorPredicate::Color(CardColorSelector::Blue),
1404 "black" => SelectorPredicate::Color(CardColorSelector::Black),
1405 "red" => SelectorPredicate::Color(CardColorSelector::Red),
1406 "green" => SelectorPredicate::Color(CardColorSelector::Green),
1407 "colorless" => SelectorPredicate::Colorless,
1408 "creature" => SelectorPredicate::CardType(CardSelectorType::Creature),
1409 "land" => SelectorPredicate::CardType(CardSelectorType::Land),
1410 "artifact" => SelectorPredicate::CardType(CardSelectorType::Artifact),
1411 "enchantment" => SelectorPredicate::CardType(CardSelectorType::Enchantment),
1412 "legendary" => SelectorPredicate::Legendary,
1413 "basic" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Basic),
1414 "snow" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Snow),
1415 "token" => SelectorPredicate::Token(true),
1416 _ => SelectorPredicate::CardType(CardSelectorType::Subtype(value[3..].to_string())),
1417 };
1418 Some(SelectorPredicate::Not(Box::new(positive)))
1419}
1420
1421fn lower_keyword_predicate(value: &str) -> Option<SelectorPredicate> {
1422 let lower = value.to_ascii_lowercase();
1423 if lower.starts_with("without") && value.len() > 7 {
1424 return Some(SelectorPredicate::Keyword {
1425 name: value[7..].to_string(),
1426 present: false,
1427 });
1428 }
1429 if lower.starts_with("with") && value.len() > 4 {
1430 return Some(SelectorPredicate::Keyword {
1431 name: value[4..].to_string(),
1432 present: true,
1433 });
1434 }
1435 None
1436}
1437
1438fn lower_subtype_predicate(value: &str) -> Option<SelectorPredicate> {
1439 let lower = value.to_ascii_lowercase();
1440 let structured = lower.starts_with("cmc")
1441 || lower.starts_with("power")
1442 || lower.starts_with("toughness")
1443 || lower.starts_with("counters_")
1444 || lower.starts_with("wascastfrom")
1445 || lower.starts_with("shares")
1446 || lower.starts_with("attached")
1447 || lower.starts_with("controlledby ")
1448 || lower.ends_with("source")
1449 || value.contains(' ')
1450 || value.contains('_')
1451 || value.contains('/');
1452 if structured
1453 || value.is_empty()
1454 || !value
1455 .chars()
1456 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '\'')
1457 {
1458 return None;
1459 }
1460 Some(SelectorPredicate::CardType(CardSelectorType::Subtype(
1461 value.to_string(),
1462 )))
1463}
1464
1465fn lower_selector_comparison(value: &str) -> Option<SelectorPredicate> {
1466 let lower = value.to_ascii_lowercase();
1467 let (property, rest) = if lower.starts_with("cmc") {
1468 (NumericSelectorProperty::ManaValue, &value[3..])
1469 } else if lower.starts_with("power") {
1470 (NumericSelectorProperty::Power, &value[5..])
1471 } else if lower.starts_with("toughness") {
1472 (NumericSelectorProperty::Toughness, &value[9..])
1473 } else if lower.starts_with("numtargets ") {
1474 (NumericSelectorProperty::TargetCount, value[11..].trim())
1475 } else if lower.starts_with("manaspent ") {
1476 (NumericSelectorProperty::ManaSpent, value[10..].trim())
1477 } else {
1478 return None;
1479 };
1480 if rest.eq_ignore_ascii_case("even") {
1481 return Some(SelectorPredicate::NumericParity {
1482 property,
1483 even: true,
1484 });
1485 }
1486 if rest.eq_ignore_ascii_case("odd") {
1487 return Some(SelectorPredicate::NumericParity {
1488 property,
1489 even: false,
1490 });
1491 }
1492 let (operator, value) = parse_selector_comparison(rest)?;
1493 Some(SelectorPredicate::NumericComparison {
1494 property,
1495 operator,
1496 value,
1497 })
1498}
1499
1500fn lower_counter_comparison(value: &str) -> Option<SelectorPredicate> {
1501 let rest = value.strip_prefix("counters_")?;
1502 let operator = parse_selector_operator(rest.get(..2)?)?;
1503 let after_op = rest.get(2..)?;
1504 let split = after_op.find('_')?;
1505 let value = parse_selector_operand(&after_op[..split])?;
1506 let counter_type = after_op[split + 1..].to_string();
1507 Some(SelectorPredicate::CounterComparison {
1508 operator,
1509 value,
1510 counter_type,
1511 })
1512}
1513
1514fn parse_selector_comparison(
1515 rest: &str,
1516) -> Option<(SelectorCompareOperator, SelectorNumericOperand)> {
1517 let operator = parse_selector_operator(rest.get(..2)?)?;
1518 let value = parse_selector_operand(rest.get(2..)?)?;
1519 Some((operator, value))
1520}
1521
1522fn parse_selector_operand(value: &str) -> Option<SelectorNumericOperand> {
1523 if value.is_empty() {
1524 return None;
1525 }
1526 if let Ok(value) = value.parse::<i32>() {
1527 Some(SelectorNumericOperand::Literal(value))
1528 } else if value
1529 .chars()
1530 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
1531 {
1532 Some(SelectorNumericOperand::Symbol(value.to_string()))
1533 } else {
1534 None
1535 }
1536}
1537
1538fn parse_selector_operator(operator: &str) -> Option<SelectorCompareOperator> {
1539 match operator.to_ascii_lowercase().as_str() {
1540 "eq" => Some(SelectorCompareOperator::Eq),
1541 "ne" => Some(SelectorCompareOperator::Ne),
1542 "lt" => Some(SelectorCompareOperator::Lt),
1543 "le" => Some(SelectorCompareOperator::Le),
1544 "gt" => Some(SelectorCompareOperator::Gt),
1545 "ge" => Some(SelectorCompareOperator::Ge),
1546 _ => None,
1547 }
1548}
1549
1550#[cfg(debug_assertions)]
1551fn matches_selector_semantics(key: &str, value: &str) -> bool {
1552 matches!(
1553 parse_semantic_param_value(key, value),
1554 SemanticParamValue::Selector(_)
1555 | SemanticParamValue::Reference(_)
1556 | SemanticParamValue::SVarReference(_)
1557 | SemanticParamValue::DelimitedList(_)
1558 | SemanticParamValue::Raw(_)
1559 )
1560}
1561
1562#[cfg(debug_assertions)]
1563fn matches_reference_semantics(key: &str, value: &str) -> bool {
1564 matches!(
1565 parse_semantic_param_value(key, value),
1566 SemanticParamValue::Reference(_)
1567 | SemanticParamValue::Selector(_)
1568 | SemanticParamValue::SVarReference(_)
1569 | SemanticParamValue::Raw(_)
1570 )
1571}
1572
1573#[cfg(debug_assertions)]
1574fn legacy_parse_params(raw: &str) -> BTreeMap<String, String> {
1575 let mut map = BTreeMap::new();
1576 for part in raw.split('|') {
1577 let part = part.trim();
1578 if let Some(idx) = part.find("$ ") {
1579 let key = part[..idx].trim().to_string();
1580 let value = part[idx + 2..].trim().to_string();
1581 map.insert(key, value);
1582 } else if let Some(idx) = part.find('$') {
1583 let key = part[..idx].trim().to_string();
1584 let value = part[idx + 1..].trim().to_string();
1585 map.insert(key, value);
1586 }
1587 }
1588 map
1589}
1590
1591pub fn parse_classified_or_warn<T>(result: Option<T>, kind: &str, raw: &str) -> Option<T> {
1605 if result.is_none() {
1606 let preview: String = raw.trim().chars().take(100).collect();
1607 eprintln!("[parse] failed to parse {kind} from: {preview}");
1608 }
1609 result
1610}
1611
1612pub fn parse_or_warn<T>(result: Option<T>, kind: &str, raw: &str) -> Option<T> {
1613 if result.is_none() {
1614 let trimmed = raw.trim();
1615 let should_warn = match kind {
1616 "StaticAbility" => trimmed.starts_with("S$") || trimmed.starts_with("S:"),
1617 "ReplacementEffect" => trimmed.starts_with("R$") || trimmed.starts_with("R:"),
1618 "Trigger" => trimmed.starts_with("T$") || trimmed.starts_with("T:"),
1619 "ActivatedAbility" => {
1620 trimmed.starts_with("AB$") || trimmed.starts_with("AB:")
1624 }
1625 _ => false,
1626 };
1627 if should_warn {
1628 let preview: String = trimmed.chars().take(100).collect();
1629 eprintln!("[parse] failed to parse {} from: {}", kind, preview);
1630 }
1631 }
1632 result
1633}
1634
1635impl From<BTreeMap<String, String>> for Params {
1638 fn from(map: BTreeMap<String, String>) -> Self {
1639 Params::from_map(map)
1640 }
1641}
1642
1643impl From<Params> for BTreeMap<String, String> {
1644 fn from(params: Params) -> Self {
1645 params.0
1646 }
1647}
1648
1649pub fn strip_times_multiplier(s: &str) -> (&str, i32) {
1657 if let Some(idx) = s.find("/Times.") {
1658 let mult_str = &s[idx + 7..];
1659 let mult = mult_str.parse::<i32>().unwrap_or(1);
1660 (&s[..idx], mult)
1661 } else {
1662 (s, 1)
1663 }
1664}
1665
1666fn normalize_enchant_type(enchant_type: &str) -> &str {
1671 enchant_type
1672 .split_once(':')
1673 .map(|(kind, _)| kind)
1674 .unwrap_or(enchant_type)
1675 .trim()
1676}
1677
1678pub fn enchant_type_to_valid_tgts(enchant_type: &str) -> String {
1679 let normalized = normalize_enchant_type(enchant_type).trim();
1680 match normalized.to_lowercase().as_str() {
1681 "creature" => "Creature".to_string(),
1682 "land" => "Land".to_string(),
1683 "artifact" => "Artifact".to_string(),
1684 "enchantment" => "Enchantment".to_string(),
1685 "planeswalker" => "Planeswalker".to_string(),
1686 "permanent" | "" => "Permanent".to_string(),
1687 "player" => "Player".to_string(),
1688 "creature or player" => "Creature,Player".to_string(),
1689 _ => normalized.to_string(),
1693 }
1694}
1695
1696pub fn enchant_type_to_target_params(enchant_type: &str) -> String {
1699 let normalized = normalize_enchant_type(enchant_type);
1700 let lower = normalized.to_lowercase();
1701 if lower == "creature.inzonegraveyard" {
1702 return "Origin$ Graveyard | ValidTgts$ Creature".to_string();
1703 }
1704 format!("ValidTgts$ {}", enchant_type_to_valid_tgts(normalized))
1705}
1706
1707pub fn enchant_type_matches_card(
1712 enchant_type: &str,
1713 card: &crate::card::CardInstance,
1714 aura_source: Option<&crate::card::CardInstance>,
1715) -> bool {
1716 let normalized = normalize_enchant_type(enchant_type).trim();
1717 match normalized.to_lowercase().as_str() {
1718 "creature" => card.zone == ZoneType::Battlefield && card.is_creature(),
1719 "creature.inzonegraveyard" => card.zone == ZoneType::Graveyard && card.is_creature(),
1720 "land" => card.zone == ZoneType::Battlefield && card.is_land(),
1721 "artifact" => card.zone == ZoneType::Battlefield && card.type_line.is_artifact(),
1722 "enchantment" => card.zone == ZoneType::Battlefield && card.type_line.is_enchantment(),
1723 "planeswalker" => card.zone == ZoneType::Battlefield && card.type_line.is_planeswalker(),
1724 "permanent" | "" => card.zone == ZoneType::Battlefield,
1725 _ => {
1726 if card.zone != ZoneType::Battlefield {
1734 return false;
1735 }
1736 let source = aura_source.unwrap_or(card);
1737 crate::card::valid_filter::matches_valid_card(normalized, card, source)
1738 }
1739 }
1740}
1741
1742#[cfg(test)]
1743mod tests {
1744 use super::*;
1745
1746 #[test]
1747 fn parse_basic_pipe_params() {
1748 let params = Params::from_raw("Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield");
1749 assert_eq!(params.get("Mode"), Some("ChangesZone"));
1750 assert_eq!(params.get("Origin"), Some("Any"));
1751 assert_eq!(params.get("Destination"), Some("Battlefield"));
1752 }
1753
1754 #[test]
1755 fn parse_bare_dollar_no_space() {
1756 let params = Params::from_raw("Key$Value | Other$ Spaced");
1758 assert_eq!(params.get("Key"), Some("Value"));
1759 assert_eq!(params.get("Other"), Some("Spaced"));
1760 }
1761
1762 #[test]
1763 fn is_true_case_insensitive() {
1764 let params = Params::from_raw("Hidden$ True | Mandatory$ true | Other$ false");
1765 assert!(params.is_true("Hidden"));
1766 assert!(params.is_true("Mandatory"));
1767 assert!(!params.is_true("Other"));
1768 assert!(!params.is_true("Missing"));
1769 }
1770
1771 #[test]
1772 fn numeric_accessors() {
1773 let params = Params::from_raw("Amount$ 3 | Bad$ notanumber");
1774 assert_eq!(params.as_i32("Amount"), Some(3));
1775 assert_eq!(params.as_usize("Amount"), Some(3));
1776 assert_eq!(params.as_i32("Bad"), None);
1777 assert_eq!(params.as_i32("Missing"), None);
1778 }
1779
1780 #[test]
1781 fn get_or_default_works() {
1782 let params = Params::from_raw("Mode$ Continuous");
1783 assert_eq!(params.get_or_default("Mode", "None"), "Continuous");
1784 assert_eq!(params.get_or_default("Missing", "fallback"), "fallback");
1785 }
1786
1787 #[test]
1788 fn serde_roundtrip() {
1789 let params = Params::from_raw("Mode$ Test | Amount$ 5");
1790 let json = serde_json::to_string(¶ms).unwrap();
1791 let deserialized: Params = serde_json::from_str(&json).unwrap();
1792 assert_eq!(deserialized.get("Mode"), Some("Test"));
1793 assert_eq!(deserialized.get("Amount"), Some("5"));
1794 }
1795}