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 {key} diverged from string params"
594 );
595 result
596 }
597
598 pub fn as_i32<K: AsRef<str>>(&self, key: K) -> Option<i32> {
600 crate::perf::increment_params_lookup();
601 let key = key.as_ref();
602 let result = self.0.get(key).and_then(|value| semantic_i32(key, value));
603 #[cfg(debug_assertions)]
604 debug_assert_eq!(
605 result,
606 self.0.get(key).and_then(|value| value.trim().parse().ok()),
607 "semantic i32 param {key} diverged from string params"
608 );
609 result
610 }
611
612 pub fn as_usize<K: AsRef<str>>(&self, key: K) -> Option<usize> {
614 crate::perf::increment_params_lookup();
615 let key = key.as_ref();
616 let result = self
617 .0
618 .get(key)
619 .and_then(|value| semantic_i32(key, value))
620 .and_then(|value| usize::try_from(value).ok());
621 #[cfg(debug_assertions)]
622 debug_assert_eq!(
623 result,
624 self.0.get(key).and_then(|value| value.trim().parse().ok()),
625 "semantic usize param {key} diverged from string params"
626 );
627 result
628 }
629
630 pub fn zone_type<K: AsRef<str>>(&self, key: K) -> Option<ZoneType> {
632 crate::perf::increment_params_lookup();
633 let key = key.as_ref();
634 let result = self
635 .0
636 .get(key)
637 .and_then(|value| semantic_zone_type(key, value));
638 #[cfg(debug_assertions)]
639 debug_assert_eq!(
640 result,
641 self.0.get(key).and_then(|value| legacy_zone_type(value)),
642 "semantic zone param {key} diverged from string params"
643 );
644 result
645 }
646
647 pub fn zone_types<K: AsRef<str>>(&self, key: K) -> Vec<ZoneType> {
649 crate::perf::increment_params_lookup();
650 let key = key.as_ref();
651 let result = self
652 .0
653 .get(key)
654 .map(|value| semantic_zone_types(key, value))
655 .unwrap_or_default();
656 #[cfg(debug_assertions)]
657 debug_assert_eq!(
658 result,
659 self.0
660 .get(key)
661 .map(|value| legacy_zone_types(value))
662 .unwrap_or_default(),
663 "semantic zone-list param {key} diverged from string params"
664 );
665 result
666 }
667
668 pub fn selector_value<K: AsRef<str>>(&self, key: K) -> Option<&str> {
672 crate::perf::increment_params_lookup();
673 let key = key.as_ref();
674 let value = self.0.get(key).map(String::as_str)?;
675 #[cfg(debug_assertions)]
676 debug_assert!(
677 matches_selector_semantics(key, value),
678 "semantic selector param {} classified unexpectedly: {:?}",
679 key,
680 parse_semantic_param_value(key, value)
681 );
682 Some(value)
683 }
684
685 pub fn selector<K: AsRef<str>>(&self, key: K) -> Option<&CompiledSelector> {
687 crate::perf::increment_params_lookup();
688 let key = key.as_ref();
689 self.selector_untracked(key)
690 }
691
692 pub fn selector_untracked<K: AsRef<str>>(&self, key: K) -> Option<&CompiledSelector> {
693 let key = key.as_ref();
694 match self.1.get(key) {
695 Some(CompiledParamValue::Selector(selector))
696 | Some(CompiledParamValue::Reference(selector)) => Some(selector),
697 None => None,
698 }
699 }
700
701 pub fn selector_cloned<K: AsRef<str>>(&self, key: K) -> Option<CompiledSelector> {
708 crate::perf::increment_params_lookup();
709 let key = key.as_ref();
710 match self.1.get(key) {
711 Some(CompiledParamValue::Selector(selector))
712 | Some(CompiledParamValue::Reference(selector)) => Some(selector.clone()),
713 None => self.0.get(key).map(|value| cached_compiled_selector(value)),
714 }
715 }
716
717 pub fn selector_cloned_any(&self, keys: &[&str]) -> Option<CompiledSelector> {
718 keys.iter().find_map(|key| self.selector_cloned(*key))
719 }
720
721 pub fn reference_value<K: AsRef<str>>(&self, key: K) -> Option<&str> {
724 crate::perf::increment_params_lookup();
725 let key = key.as_ref();
726 let value = self.0.get(key).map(String::as_str)?;
727 #[cfg(debug_assertions)]
728 debug_assert!(
729 matches_reference_semantics(key, value),
730 "semantic reference param {} classified unexpectedly: {:?}",
731 key,
732 parse_semantic_param_value(key, value)
733 );
734 Some(value)
735 }
736
737 pub fn reference<K: AsRef<str>>(&self, key: K) -> Option<&CompiledSelector> {
739 crate::perf::increment_params_lookup();
740 let key = key.as_ref();
741 match self.1.get(key) {
742 Some(CompiledParamValue::Reference(selector))
743 | Some(CompiledParamValue::Selector(selector)) => Some(selector),
744 None => None,
745 }
746 }
747
748 pub fn get_cloned(&self, key: &str) -> Option<String> {
750 self.0.get(key).cloned()
751 }
752
753 pub fn split_param_list<K: AsRef<str>>(&self, key: K, delimiter: &str) -> Vec<String> {
755 split_param_list_value(self.get(key), delimiter)
756 }
757
758 pub fn semantic_value<K: AsRef<str>>(&self, key: K) -> Option<SemanticParamValue<'_>> {
763 crate::perf::increment_params_lookup();
764 let key = key.as_ref();
765 self.0
766 .get(key)
767 .map(|value| parse_semantic_param_value(key, value))
768 }
769
770 pub fn require(&self, key: &str, context: &str) -> Option<&str> {
777 match self.get(key) {
778 Some(v) => Some(v),
779 None => {
780 eprintln!("[parse] missing required param '{key}' in {context}");
781 None
782 }
783 }
784 }
785
786 pub fn require_cloned(&self, key: &str, context: &str) -> Option<String> {
788 self.require(key, context).map(|s| s.to_string())
789 }
790
791 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
795 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
796 }
797}
798
799pub fn split_param_list_value(value: Option<&str>, delimiter: &str) -> Vec<String> {
800 value
801 .map(|value| {
802 value
803 .split(delimiter)
804 .map(str::trim)
805 .filter(|part| !part.is_empty())
806 .map(str::to_owned)
807 .collect()
808 })
809 .unwrap_or_default()
810}
811
812fn semantic_i32(key: &str, value: &str) -> Option<i32> {
813 match parse_semantic_param_value(key, value) {
814 SemanticParamValue::Integer(value) => Some(value),
815 SemanticParamValue::Amount(SemanticAmount::Literal(value)) => Some(value),
816 _ => value.trim().parse().ok(),
817 }
818}
819
820fn semantic_zone_type(key: &str, value: &str) -> Option<ZoneType> {
821 match parse_semantic_param_value(key, value) {
822 SemanticParamValue::ZoneList(zones) if zones.len() == 1 => {
823 zones.first().and_then(|zone| legacy_zone_type(zone))
824 }
825 SemanticParamValue::ZoneList(_) => None,
826 _ => legacy_zone_type(value),
827 }
828}
829
830fn semantic_zone_types(key: &str, value: &str) -> Vec<ZoneType> {
831 match parse_semantic_param_value(key, value) {
832 SemanticParamValue::ZoneList(zones) => {
833 zones.into_iter().filter_map(legacy_zone_type).collect()
834 }
835 _ => legacy_zone_types(value),
836 }
837}
838
839fn legacy_zone_type(value: &str) -> Option<ZoneType> {
840 let value = value.trim();
841 if value.eq_ignore_ascii_case("Deck") {
842 Some(ZoneType::Library)
843 } else {
844 ZoneType::from_str_compat(value)
845 }
846}
847
848fn legacy_zone_types(value: &str) -> Vec<ZoneType> {
849 value
850 .split([',', ' '])
851 .map(str::trim)
852 .filter(|part| !part.is_empty())
853 .filter_map(legacy_zone_type)
854 .collect()
855}
856
857fn compile_param_map(map: &BTreeMap<String, String>) -> BTreeMap<String, CompiledParamValue> {
858 map.iter()
859 .filter_map(|(key, value)| {
860 compile_semantic_param_value(&parse_semantic_param_value(key, value))
861 .map(|compiled| (key.clone(), compiled))
862 })
863 .collect()
864}
865
866fn update_compiled_param(
867 compiled: &mut BTreeMap<String, CompiledParamValue>,
868 key: &str,
869 value: &str,
870) {
871 match compile_semantic_param_value(&parse_semantic_param_value(key, value)) {
872 Some(value) => {
873 compiled.insert(key.to_string(), value);
874 }
875 None => {
876 compiled.remove(key);
877 }
878 }
879}
880
881fn compile_semantic_param_value(value: &SemanticParamValue<'_>) -> Option<CompiledParamValue> {
882 match value {
883 SemanticParamValue::Selector(selector) => Some(CompiledParamValue::Selector(
884 compile_semantic_selector(selector),
885 )),
886 SemanticParamValue::Reference(selector) => Some(CompiledParamValue::Reference(
887 compile_semantic_selector(selector),
888 )),
889 _ => None,
890 }
891}
892
893fn compile_semantic_selector(selector: &SemanticSelector<'_>) -> CompiledSelector {
894 let alternatives = selector
895 .alternatives
896 .iter()
897 .map(|alternative| CompiledSelectorAlternative {
898 raw: alternative.raw.to_string(),
899 parts: alternative
900 .parts
901 .iter()
902 .map(|part| CompiledSelectorPart {
903 separator: part.separator,
904 value: part.value.to_string(),
905 })
906 .collect(),
907 })
908 .collect::<Vec<_>>();
909 let ir = lower_compiled_selector(&alternatives);
910 CompiledSelector { alternatives, ir }
911}
912
913fn lower_compiled_selector(alternatives: &[CompiledSelectorAlternative]) -> Selector {
914 Selector {
915 alternatives: alternatives
916 .iter()
917 .map(|alternative| {
918 let mut predicates: Vec<_> = alternative
919 .parts
920 .iter()
921 .enumerate()
922 .map(|(idx, part)| lower_selector_part(&part.value, idx == 0))
923 .collect();
924 predicates.sort_by_key(selector_predicate_order);
925 SelectorAlt { predicates }
926 })
927 .collect(),
928 }
929}
930
931fn selector_predicate_order(predicate: &SelectorPredicate) -> u8 {
932 match predicate {
933 SelectorPredicate::Any
934 | SelectorPredicate::CardType(_)
935 | SelectorPredicate::CardSupertype(_)
936 | SelectorPredicate::Zone(_)
937 | SelectorPredicate::CardController(_)
938 | SelectorPredicate::CardOwner(_)
939 | SelectorPredicate::Tapped(_)
940 | SelectorPredicate::StartedTurnTapped(_)
941 | SelectorPredicate::CameUnderControlSinceLastUpkeep
942 | SelectorPredicate::Token(_)
943 | SelectorPredicate::Color(_)
944 | SelectorPredicate::Multicolor
945 | SelectorPredicate::Colorless
946 | SelectorPredicate::Commander
947 | SelectorPredicate::Legendary
948 | SelectorPredicate::Kicked => 0,
949 SelectorPredicate::CardIdentity(_) | SelectorPredicate::PlayerController(_) => 1,
950 SelectorPredicate::NumericComparison { .. }
951 | SelectorPredicate::NumericParity { .. }
952 | SelectorPredicate::CounterComparison { .. }
953 | SelectorPredicate::Keyword { .. }
954 | SelectorPredicate::CardState(_)
955 | SelectorPredicate::ChosenType
956 | SelectorPredicate::WasCast { .. }
957 | SelectorPredicate::SourceColor(_)
958 | SelectorPredicate::SourceColorless
959 | SelectorPredicate::ChosenColorSource => 2,
960 SelectorPredicate::Context(_) => 3,
961 SelectorPredicate::Relation(_) => 4,
962 SelectorPredicate::RememberedCard
963 | SelectorPredicate::TriggerRememberedCard
964 | SelectorPredicate::EffectSource
965 | SelectorPredicate::DamagedBy
966 | SelectorPredicate::AttachedBy => 5,
967 SelectorPredicate::Not(inner) => selector_predicate_order(inner).saturating_add(1),
968 SelectorPredicate::Player => 6,
969 SelectorPredicate::Raw(_) => 7,
970 }
971}
972
973fn lower_selector_part(value: &str, is_first_part: bool) -> SelectorPredicate {
974 let normalized = value.trim();
975 if normalized.is_empty() {
976 return SelectorPredicate::Raw(String::new());
977 }
978 let lower = normalized.to_ascii_lowercase();
979 if is_first_part {
980 return match lower.as_str() {
981 "any" => SelectorPredicate::Any,
982 "card" => SelectorPredicate::CardType(CardSelectorType::Card),
983 "creature" => SelectorPredicate::CardType(CardSelectorType::Creature),
984 "land" => SelectorPredicate::CardType(CardSelectorType::Land),
985 "instant" => SelectorPredicate::CardType(CardSelectorType::Instant),
986 "sorcery" => SelectorPredicate::CardType(CardSelectorType::Sorcery),
987 "artifact" => SelectorPredicate::CardType(CardSelectorType::Artifact),
988 "enchantment" => SelectorPredicate::CardType(CardSelectorType::Enchantment),
989 "planeswalker" => SelectorPredicate::CardType(CardSelectorType::Planeswalker),
990 "permanent" => SelectorPredicate::CardType(CardSelectorType::Permanent),
991 "spell" => SelectorPredicate::CardType(CardSelectorType::Spell),
992 "nonland" => SelectorPredicate::CardType(CardSelectorType::NonLand),
993 "noncreature" => SelectorPredicate::CardType(CardSelectorType::NonCreature),
994 "player" | "each" | "player.ingame" => SelectorPredicate::Player,
995 "you" | "youctrl" => SelectorPredicate::PlayerController(ControllerSelector::You),
996 "opponent" | "oppctrl" | "opponentctrl" => {
997 SelectorPredicate::PlayerController(ControllerSelector::Opponent)
998 }
999 named if named.starts_with("named") => SelectorPredicate::CardType(
1000 CardSelectorType::Named(normalized[5..].trim().to_string()),
1001 ),
1002 _ => SelectorPredicate::CardType(CardSelectorType::Subtype(normalized.to_string())),
1003 };
1004 }
1005
1006 if let Some(stripped) = normalized.strip_prefix('!') {
1007 let predicate = lower_selector_part(stripped, false);
1008 return SelectorPredicate::Not(Box::new(predicate));
1009 }
1010
1011 match lower.as_str() {
1012 "self" | "strictlyself" => SelectorPredicate::CardIdentity(CardIdentitySelector::Self_),
1013 "other" | "strictlyother" => SelectorPredicate::CardIdentity(CardIdentitySelector::Other),
1014 "you" | "youctrl" | "youcontrol" => {
1015 SelectorPredicate::CardController(ControllerSelector::You)
1016 }
1017 "opponent" | "oppctrl" | "opponentctrl" => {
1018 SelectorPredicate::CardController(ControllerSelector::Opponent)
1019 }
1020 "chosenctrl" => SelectorPredicate::Raw(normalized.to_string()),
1021 "youown" => SelectorPredicate::CardOwner(ControllerSelector::You),
1022 "oppown" | "opponentown" => SelectorPredicate::CardOwner(ControllerSelector::Opponent),
1023 "targetedplayerown" | "targetedown" | "targetedowner" => {
1026 SelectorPredicate::Raw(normalized.to_string())
1027 }
1028 "youdontctrl" => SelectorPredicate::Not(Box::new(SelectorPredicate::CardController(
1029 ControllerSelector::You,
1030 ))),
1031 "youdontown" => SelectorPredicate::Not(Box::new(SelectorPredicate::CardOwner(
1032 ControllerSelector::You,
1033 ))),
1034 "tapped" => SelectorPredicate::Tapped(true),
1035 "untapped" => SelectorPredicate::Tapped(false),
1036 "startedtheturntapped" => SelectorPredicate::StartedTurnTapped(true),
1037 "startedtheturnuntapped" => SelectorPredicate::StartedTurnTapped(false),
1038 "cameundercontrolsincelastupkeep" => SelectorPredicate::CameUnderControlSinceLastUpkeep,
1039 "inzonebattlefield" => SelectorPredicate::Zone(ZoneType::Battlefield),
1040 "inzonegraveyard" => SelectorPredicate::Zone(ZoneType::Graveyard),
1041 "inzonehand" => SelectorPredicate::Zone(ZoneType::Hand),
1042 "inzoneexile" => SelectorPredicate::Zone(ZoneType::Exile),
1043 "inzonestack" => SelectorPredicate::Zone(ZoneType::Stack),
1044 "isremembered" => SelectorPredicate::RememberedCard,
1045 "istriggerremembered" => SelectorPredicate::TriggerRememberedCard,
1046 "effectsource" => SelectorPredicate::EffectSource,
1047 "iscommander" => SelectorPredicate::Commander,
1048 "legendary" => SelectorPredicate::Legendary,
1049 "basic" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Basic),
1050 "snow" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Snow),
1051 "kicked" => SelectorPredicate::Kicked,
1052 "token" => SelectorPredicate::Token(true),
1053 "nontoken" => SelectorPredicate::Token(false),
1054 "creature" => SelectorPredicate::CardType(CardSelectorType::Creature),
1055 "land" => SelectorPredicate::CardType(CardSelectorType::Land),
1056 "instant" => SelectorPredicate::CardType(CardSelectorType::Instant),
1057 "sorcery" => SelectorPredicate::CardType(CardSelectorType::Sorcery),
1058 "artifact" => SelectorPredicate::CardType(CardSelectorType::Artifact),
1059 "enchantment" => SelectorPredicate::CardType(CardSelectorType::Enchantment),
1060 "planeswalker" => SelectorPredicate::CardType(CardSelectorType::Planeswalker),
1061 "permanent" => SelectorPredicate::CardType(CardSelectorType::Permanent),
1062 "noncreature" => SelectorPredicate::CardType(CardSelectorType::NonCreature),
1063 "nonland" => SelectorPredicate::CardType(CardSelectorType::NonLand),
1064 "time lord" => {
1065 SelectorPredicate::CardType(CardSelectorType::Subtype("Time Lord".to_string()))
1066 }
1067 "white" => SelectorPredicate::Color(CardColorSelector::White),
1068 "blue" => SelectorPredicate::Color(CardColorSelector::Blue),
1069 "black" => SelectorPredicate::Color(CardColorSelector::Black),
1070 "red" => SelectorPredicate::Color(CardColorSelector::Red),
1071 "green" => SelectorPredicate::Color(CardColorSelector::Green),
1072 "multicolor" => SelectorPredicate::Multicolor,
1073 "colorless" => SelectorPredicate::Colorless,
1074 "whitesource" => SelectorPredicate::SourceColor(CardColorSelector::White),
1075 "bluesource" => SelectorPredicate::SourceColor(CardColorSelector::Blue),
1076 "blacksource" => SelectorPredicate::SourceColor(CardColorSelector::Black),
1077 "redsource" => SelectorPredicate::SourceColor(CardColorSelector::Red),
1078 "greensource" => SelectorPredicate::SourceColor(CardColorSelector::Green),
1079 "colorlesssource" => SelectorPredicate::SourceColorless,
1080 "chosencolorsource" => SelectorPredicate::ChosenColorSource,
1081 "attacking" => SelectorPredicate::Context(ContextPredicate::Attacking(None)),
1082 "attackingyou" => {
1083 SelectorPredicate::Context(ContextPredicate::Attacking(Some(TargetRef::Source)))
1084 }
1085 "blocking" => SelectorPredicate::Context(ContextPredicate::Blocking(None)),
1086 "blocked" => SelectorPredicate::Context(ContextPredicate::Blocked),
1087 "attackedthisturn" => SelectorPredicate::Context(ContextPredicate::AttackedThisTurn),
1088 "blockingsource" => SelectorPredicate::Context(ContextPredicate::BlockingSource),
1089 "blockedbysource" => SelectorPredicate::Context(ContextPredicate::BlockedBySource),
1090 "samename" => {
1091 SelectorPredicate::Relation(RelationPredicate::SharesNameWith(TargetRef::Source))
1092 }
1093 "damagedby" => SelectorPredicate::DamagedBy,
1094 "equippedby" | "enchantedby" | "attachedby" => SelectorPredicate::AttachedBy,
1095 "facedown" => SelectorPredicate::CardState(CardStateSelector::FaceDown),
1096 "paired" => SelectorPredicate::CardState(CardStateSelector::Paired),
1097 "pairedwith" => SelectorPredicate::CardState(CardStateSelector::PairedWithSource),
1098 "attached" => SelectorPredicate::CardState(CardStateSelector::Attached),
1099 "equipped" => SelectorPredicate::CardState(CardStateSelector::Equipped),
1100 "enchanted" => SelectorPredicate::CardState(CardStateSelector::Enchanted),
1101 "hascounters" => SelectorPredicate::CardState(CardStateSelector::HasCounters),
1102 "isimprinted" => SelectorPredicate::CardState(CardStateSelector::IsImprinted),
1103 "chosen" => SelectorPredicate::CardState(CardStateSelector::Chosen),
1104 "chosencard" | "chosencardstrict" => {
1105 SelectorPredicate::CardState(CardStateSelector::ChosenCard)
1106 }
1107 "namedcard" => SelectorPredicate::CardState(CardStateSelector::NamedCard),
1108 "chosencolor" => SelectorPredicate::CardState(CardStateSelector::ChosenColor),
1109 "thisturnentered" => SelectorPredicate::CardState(CardStateSelector::EnteredThisTurn),
1110 "wasdealtdamagethisturn" => {
1111 SelectorPredicate::CardState(CardStateSelector::WasDealtDamageThisTurn)
1112 }
1113 "historic" => SelectorPredicate::CardState(CardStateSelector::Historic),
1114 "modified" => SelectorPredicate::CardState(CardStateSelector::Modified),
1115 "issaddled" => SelectorPredicate::CardState(CardStateSelector::Saddled),
1116 "mayplaysource" => SelectorPredicate::CardState(CardStateSelector::MayPlaySource),
1117 "exiledwithsource" => SelectorPredicate::Context(ContextPredicate::ExiledWithSource),
1118 "toplibrary" => SelectorPredicate::Context(ContextPredicate::TopLibrary),
1119 "suspended" => SelectorPredicate::CardState(CardStateSelector::Suspended),
1120 "singletarget" => SelectorPredicate::CardState(CardStateSelector::SingleTarget),
1121 "promisedgift" => SelectorPredicate::CardState(CardStateSelector::PromisedGift),
1122 "isringbearer" => SelectorPredicate::CardState(CardStateSelector::RingBearer),
1123 "wascast" => SelectorPredicate::WasCast { by_you: false },
1124 "wascastbyyou" => SelectorPredicate::WasCast { by_you: true },
1125 named if named.starts_with("named") => {
1126 SelectorPredicate::CardType(CardSelectorType::Named(normalized[5..].trim().to_string()))
1127 }
1128 cast_origin if cast_origin.starts_with("wascastfrom") => {
1129 lower_cast_origin_predicate(normalized)
1130 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1131 }
1132 "chosentype" => SelectorPredicate::ChosenType,
1133 "rememberedplayerctrl" => {
1134 SelectorPredicate::Context(ContextPredicate::RememberedPlayerCtrl)
1135 }
1136 "targetedplayerctrl" => SelectorPredicate::Context(ContextPredicate::TargetedPlayerCtrl),
1137 "activeplayerctrl" => SelectorPredicate::Context(ContextPredicate::ActivePlayerCtrl),
1138 "defenderctrl" => SelectorPredicate::Context(ContextPredicate::DefenderCtrl),
1139 "enchantedcontroller" => SelectorPredicate::Context(ContextPredicate::EnchantedController),
1140 "notdefinedtargeted" => SelectorPredicate::Context(ContextPredicate::NotDefinedTargeted),
1141 controlled if controlled.starts_with("controlledby ") => SelectorPredicate::Context(
1142 ContextPredicate::ControlledBy(normalized["ControlledBy ".len()..].trim().to_string()),
1143 ),
1144 shares if shares.starts_with("sharesnamewith") => {
1145 lower_relation_target_ref(normalized["sharesNameWith".len()..].trim())
1146 .map(|target| {
1147 SelectorPredicate::Relation(RelationPredicate::SharesNameWith(target))
1148 })
1149 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1150 }
1151 shares if shares.starts_with("doesnotsharenamewith") => {
1152 lower_relation_target_ref(normalized["doesNotShareNameWith".len()..].trim())
1153 .map(|target| {
1154 SelectorPredicate::Relation(RelationPredicate::DoesNotShareNameWith(target))
1155 })
1156 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1157 }
1158 shares if shares.starts_with("sharescardtypewith") => {
1159 lower_relation_target_ref(normalized["sharesCardTypeWith".len()..].trim())
1160 .map(|target| {
1161 SelectorPredicate::Relation(RelationPredicate::SharesCardTypeWith(target))
1162 })
1163 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1164 }
1165 shares if shares.starts_with("sharescolorwith") => {
1166 lower_relation_target_ref(normalized["SharesColorWith".len()..].trim())
1167 .map(|target| {
1168 SelectorPredicate::Relation(RelationPredicate::SharesColorWith(target))
1169 })
1170 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1171 }
1172 shares if shares.starts_with("sharescmcwith") => {
1173 lower_relation_target_ref(normalized["SharesCMCWith".len()..].trim())
1174 .map(|target| {
1175 SelectorPredicate::Relation(RelationPredicate::SharesManaValueWith(target))
1176 })
1177 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1178 }
1179 shares if shares.starts_with("sharescreaturetypewith") => {
1180 lower_relation_target_ref(normalized["sharesCreatureTypeWith".len()..].trim())
1181 .map(|target| {
1182 SelectorPredicate::Relation(RelationPredicate::SharesCreatureTypeWith(target))
1183 })
1184 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1185 }
1186 attacking if attacking.starts_with("attacking ") => {
1187 lower_relation_target_ref(normalized["attacking ".len()..].trim())
1188 .map(|target| SelectorPredicate::Context(ContextPredicate::Attacking(Some(target))))
1189 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1190 }
1191 blocked_by if blocked_by.starts_with("blockedbyvalidthisturn ") => {
1192 lower_blocked_by_valid_this_turn(normalized["blockedByValidThisTurn ".len()..].trim())
1193 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1194 }
1195 blocked if blocked.starts_with("blockedvalidthisturn ") => {
1196 lower_card_selector_type(normalized["blockedValidThisTurn ".len()..].trim())
1197 .map(|card_type| {
1198 SelectorPredicate::Context(ContextPredicate::BlockedValidThisTurn(card_type))
1199 })
1200 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1201 }
1202 blocking_valid if blocking_valid.starts_with("blockingvalid ") => {
1203 lower_card_selector_type(normalized["blockingValid ".len()..].trim())
1204 .map(|card_type| {
1205 SelectorPredicate::Context(ContextPredicate::BlockingValid(card_type))
1206 })
1207 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1208 }
1209 blocking if blocking.starts_with("blocking ") => {
1210 lower_relation_target_ref(normalized["blocking ".len()..].trim())
1211 .map(|target| SelectorPredicate::Context(ContextPredicate::Blocking(Some(target))))
1212 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1213 }
1214 attached if attached.starts_with("attachedto ") => {
1215 lower_attached_to_relation(normalized["AttachedTo ".len()..].trim())
1216 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1217 }
1218 owned if owned.starts_with("ownedby ") => {
1219 lower_relation_target_ref(normalized["OwnedBy ".len()..].trim())
1220 .map(|target| SelectorPredicate::Relation(RelationPredicate::OwnedBy(target)))
1221 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1222 }
1223 opponent if opponent.starts_with("opponentof ") => {
1224 lower_relation_target_ref(normalized["OpponentOf ".len()..].trim())
1225 .map(|target| SelectorPredicate::Relation(RelationPredicate::OpponentOf(target)))
1226 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1227 }
1228 targeting if targeting.starts_with("istargeting ") => {
1229 lower_relation_target_ref(normalized["IsTargeting ".len()..].trim())
1230 .map(|target| SelectorPredicate::Relation(RelationPredicate::IsTargeting(target)))
1231 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1232 }
1233 entered if entered.starts_with("thisturnenteredfrom_") => {
1234 lower_entered_from_zone(normalized["ThisTurnEnteredFrom_".len()..].trim())
1235 .map(|zone| SelectorPredicate::Context(ContextPredicate::EnteredThisTurnFrom(zone)))
1236 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1237 }
1238 entered if entered.starts_with("enteredunder ") => {
1239 lower_relation_target_ref(normalized["EnteredUnder ".len()..].trim())
1240 .map(|target| SelectorPredicate::Context(ContextPredicate::EnteredUnder(target)))
1241 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1242 }
1243 _ => lower_non_predicate(normalized).unwrap_or_else(|| {
1244 lower_keyword_predicate(normalized).unwrap_or_else(|| {
1245 lower_selector_comparison(normalized).unwrap_or_else(|| {
1246 lower_counter_comparison(normalized).unwrap_or_else(|| {
1247 lower_subtype_predicate(normalized)
1248 .unwrap_or_else(|| SelectorPredicate::Raw(normalized.to_string()))
1249 })
1250 })
1251 })
1252 }),
1253 }
1254}
1255
1256fn lower_relation_target_ref(value: &str) -> Option<TargetRef> {
1257 if value.is_empty()
1258 || value.eq_ignore_ascii_case("Self")
1259 || value.eq_ignore_ascii_case("Source")
1260 || value.eq_ignore_ascii_case("You")
1261 || value.eq_ignore_ascii_case("YouCtrl")
1262 {
1263 Some(TargetRef::Source)
1264 } else if value.eq_ignore_ascii_case("Remembered") {
1265 Some(TargetRef::Remembered)
1266 } else if value.eq_ignore_ascii_case("RememberedLKI") {
1267 Some(TargetRef::RememberedLki)
1268 } else if value.eq_ignore_ascii_case("Imprinted") {
1269 Some(TargetRef::Imprinted)
1270 } else if value.eq_ignore_ascii_case("ChosenCard") {
1271 Some(TargetRef::ChosenCard)
1272 } else if value.eq_ignore_ascii_case("ChosenPlayer") {
1273 Some(TargetRef::ChosenPlayer)
1274 } else if value.eq_ignore_ascii_case("Targeted")
1275 || value.eq_ignore_ascii_case("TargetedPlayer")
1276 || value.eq_ignore_ascii_case("TargetedController")
1277 {
1278 Some(TargetRef::Targeted)
1279 } else if value.eq_ignore_ascii_case("Player") {
1280 Some(TargetRef::Player)
1281 } else if value.eq_ignore_ascii_case("Opponent") {
1282 Some(TargetRef::Opponent)
1283 } else if value.eq_ignore_ascii_case("Battlefield") {
1284 Some(TargetRef::Battlefield)
1285 } else if value.eq_ignore_ascii_case("OtherYourBattlefield") {
1286 Some(TargetRef::OtherYourBattlefield)
1287 } else if value.eq_ignore_ascii_case("YourGraveyard") {
1288 Some(TargetRef::YourGraveyard)
1289 } else if value.eq_ignore_ascii_case("TriggeredTarget") {
1290 Some(TargetRef::TriggeredTarget)
1291 } else if value.eq_ignore_ascii_case("TriggeredPlayer") {
1292 Some(TargetRef::TriggeredPlayer)
1293 } else if value.eq_ignore_ascii_case("TriggeredCard") {
1294 Some(TargetRef::TriggeredCard)
1295 } else if value.eq_ignore_ascii_case("TriggeredCardController") {
1296 Some(TargetRef::TriggeredCardController)
1297 } else if value.eq_ignore_ascii_case("TriggeredDefendingPlayer") {
1298 Some(TargetRef::TriggeredDefendingPlayer)
1299 } else if value.eq_ignore_ascii_case("TriggeredAttackedTarget") {
1300 Some(TargetRef::TriggeredAttackedTarget)
1301 } else if value.eq_ignore_ascii_case("Commander") {
1302 Some(TargetRef::Commander)
1303 } else {
1304 None
1305 }
1306}
1307
1308fn lower_blocked_by_valid_this_turn(value: &str) -> Option<SelectorPredicate> {
1309 if let Some(target) = lower_relation_target_ref(value) {
1310 return Some(SelectorPredicate::Context(
1311 ContextPredicate::BlockedByValidThisTurn(target),
1312 ));
1313 }
1314 lower_card_selector_type(value).map(|card_type| {
1315 SelectorPredicate::Context(ContextPredicate::BlockedByValidThisTurnType(card_type))
1316 })
1317}
1318
1319fn lower_card_selector_type(value: &str) -> Option<CardSelectorType> {
1320 match value.to_ascii_lowercase().as_str() {
1321 "card" => Some(CardSelectorType::Card),
1322 "creature" => Some(CardSelectorType::Creature),
1323 "land" => Some(CardSelectorType::Land),
1324 "artifact" => Some(CardSelectorType::Artifact),
1325 "enchantment" => Some(CardSelectorType::Enchantment),
1326 "planeswalker" => Some(CardSelectorType::Planeswalker),
1327 "permanent" => Some(CardSelectorType::Permanent),
1328 "nonland" => Some(CardSelectorType::NonLand),
1329 "noncreature" => Some(CardSelectorType::NonCreature),
1330 _ if value
1331 .chars()
1332 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '\'') =>
1333 {
1334 Some(CardSelectorType::Subtype(value.to_string()))
1335 }
1336 _ => None,
1337 }
1338}
1339
1340fn lower_entered_from_zone(value: &str) -> Option<ZoneType> {
1341 match value.to_ascii_lowercase().as_str() {
1342 "battlefield" => Some(ZoneType::Battlefield),
1343 "graveyard" => Some(ZoneType::Graveyard),
1344 "library" => Some(ZoneType::Library),
1345 "hand" => Some(ZoneType::Hand),
1346 "exile" => Some(ZoneType::Exile),
1347 _ => None,
1348 }
1349}
1350
1351fn lower_attached_to_relation(value: &str) -> Option<SelectorPredicate> {
1352 if let Some(target) = lower_relation_target_ref(value) {
1353 return Some(SelectorPredicate::Relation(RelationPredicate::AttachedTo(
1354 target,
1355 )));
1356 }
1357 let card_type = match value.to_ascii_lowercase().as_str() {
1358 "card" => CardSelectorType::Card,
1359 "creature" => CardSelectorType::Creature,
1360 "land" => CardSelectorType::Land,
1361 "artifact" => CardSelectorType::Artifact,
1362 "enchantment" => CardSelectorType::Enchantment,
1363 "permanent" => CardSelectorType::Permanent,
1364 _ => return None,
1365 };
1366 Some(SelectorPredicate::Relation(
1367 RelationPredicate::AttachedToType(card_type),
1368 ))
1369}
1370
1371fn lower_cast_origin_predicate(value: &str) -> Option<SelectorPredicate> {
1372 let lower = value.to_ascii_lowercase();
1373 let origin = match lower.as_str() {
1374 "wascastfromhand" => CastOrigin::Hand,
1375 "wascastfromyourhand" => CastOrigin::YourHand,
1376 "wascastfromyourhandbyyou" => CastOrigin::YourHandByYou,
1377 "wascastfromtheirhand" => CastOrigin::TheirHand,
1378 "wascastfromexile" => CastOrigin::Exile,
1379 "wascastfromgraveyard" => CastOrigin::Graveyard,
1380 "wascastfromyourgraveyard" => CastOrigin::YourGraveyard,
1381 "wascastfromyourgraveyardbyyou" => CastOrigin::YourGraveyardByYou,
1382 "wascastfromyourlibrary" => CastOrigin::YourLibrary,
1383 _ => return None,
1384 };
1385 Some(SelectorPredicate::Context(ContextPredicate::WasCastFrom(
1386 origin,
1387 )))
1388}
1389
1390fn lower_non_predicate(value: &str) -> Option<SelectorPredicate> {
1391 let lower = value.to_ascii_lowercase();
1392 let rest = lower.strip_prefix("non")?;
1393 if rest.is_empty() {
1394 return None;
1395 }
1396 let positive = match rest {
1397 "white" => SelectorPredicate::Color(CardColorSelector::White),
1398 "blue" => SelectorPredicate::Color(CardColorSelector::Blue),
1399 "black" => SelectorPredicate::Color(CardColorSelector::Black),
1400 "red" => SelectorPredicate::Color(CardColorSelector::Red),
1401 "green" => SelectorPredicate::Color(CardColorSelector::Green),
1402 "colorless" => SelectorPredicate::Colorless,
1403 "creature" => SelectorPredicate::CardType(CardSelectorType::Creature),
1404 "land" => SelectorPredicate::CardType(CardSelectorType::Land),
1405 "artifact" => SelectorPredicate::CardType(CardSelectorType::Artifact),
1406 "enchantment" => SelectorPredicate::CardType(CardSelectorType::Enchantment),
1407 "legendary" => SelectorPredicate::Legendary,
1408 "basic" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Basic),
1409 "snow" => SelectorPredicate::CardSupertype(CardSupertypeSelector::Snow),
1410 "token" => SelectorPredicate::Token(true),
1411 _ => SelectorPredicate::CardType(CardSelectorType::Subtype(value[3..].to_string())),
1412 };
1413 Some(SelectorPredicate::Not(Box::new(positive)))
1414}
1415
1416fn lower_keyword_predicate(value: &str) -> Option<SelectorPredicate> {
1417 let lower = value.to_ascii_lowercase();
1418 if lower.starts_with("without") && value.len() > 7 {
1419 return Some(SelectorPredicate::Keyword {
1420 name: value[7..].to_string(),
1421 present: false,
1422 });
1423 }
1424 if lower.starts_with("with") && value.len() > 4 {
1425 return Some(SelectorPredicate::Keyword {
1426 name: value[4..].to_string(),
1427 present: true,
1428 });
1429 }
1430 None
1431}
1432
1433fn lower_subtype_predicate(value: &str) -> Option<SelectorPredicate> {
1434 let lower = value.to_ascii_lowercase();
1435 let structured = lower.starts_with("cmc")
1436 || lower.starts_with("power")
1437 || lower.starts_with("toughness")
1438 || lower.starts_with("counters_")
1439 || lower.starts_with("wascastfrom")
1440 || lower.starts_with("shares")
1441 || lower.starts_with("attached")
1442 || lower.starts_with("controlledby ")
1443 || lower.ends_with("source")
1444 || value.contains(' ')
1445 || value.contains('_')
1446 || value.contains('/');
1447 if structured
1448 || value.is_empty()
1449 || !value
1450 .chars()
1451 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '\'')
1452 {
1453 return None;
1454 }
1455 Some(SelectorPredicate::CardType(CardSelectorType::Subtype(
1456 value.to_string(),
1457 )))
1458}
1459
1460fn lower_selector_comparison(value: &str) -> Option<SelectorPredicate> {
1461 let lower = value.to_ascii_lowercase();
1462 let (property, rest) = if lower.starts_with("cmc") {
1463 (NumericSelectorProperty::ManaValue, &value[3..])
1464 } else if lower.starts_with("power") {
1465 (NumericSelectorProperty::Power, &value[5..])
1466 } else if lower.starts_with("toughness") {
1467 (NumericSelectorProperty::Toughness, &value[9..])
1468 } else if lower.starts_with("numtargets ") {
1469 (NumericSelectorProperty::TargetCount, value[11..].trim())
1470 } else if lower.starts_with("manaspent ") {
1471 (NumericSelectorProperty::ManaSpent, value[10..].trim())
1472 } else {
1473 return None;
1474 };
1475 if rest.eq_ignore_ascii_case("even") {
1476 return Some(SelectorPredicate::NumericParity {
1477 property,
1478 even: true,
1479 });
1480 }
1481 if rest.eq_ignore_ascii_case("odd") {
1482 return Some(SelectorPredicate::NumericParity {
1483 property,
1484 even: false,
1485 });
1486 }
1487 let (operator, value) = parse_selector_comparison(rest)?;
1488 Some(SelectorPredicate::NumericComparison {
1489 property,
1490 operator,
1491 value,
1492 })
1493}
1494
1495fn lower_counter_comparison(value: &str) -> Option<SelectorPredicate> {
1496 let rest = value.strip_prefix("counters_")?;
1497 let operator = parse_selector_operator(rest.get(..2)?)?;
1498 let after_op = rest.get(2..)?;
1499 let split = after_op.find('_')?;
1500 let value = parse_selector_operand(&after_op[..split])?;
1501 let counter_type = after_op[split + 1..].to_string();
1502 Some(SelectorPredicate::CounterComparison {
1503 operator,
1504 value,
1505 counter_type,
1506 })
1507}
1508
1509fn parse_selector_comparison(
1510 rest: &str,
1511) -> Option<(SelectorCompareOperator, SelectorNumericOperand)> {
1512 let operator = parse_selector_operator(rest.get(..2)?)?;
1513 let value = parse_selector_operand(rest.get(2..)?)?;
1514 Some((operator, value))
1515}
1516
1517fn parse_selector_operand(value: &str) -> Option<SelectorNumericOperand> {
1518 if value.is_empty() {
1519 return None;
1520 }
1521 if let Ok(value) = value.parse::<i32>() {
1522 Some(SelectorNumericOperand::Literal(value))
1523 } else if value
1524 .chars()
1525 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
1526 {
1527 Some(SelectorNumericOperand::Symbol(value.to_string()))
1528 } else {
1529 None
1530 }
1531}
1532
1533fn parse_selector_operator(operator: &str) -> Option<SelectorCompareOperator> {
1534 match operator.to_ascii_lowercase().as_str() {
1535 "eq" => Some(SelectorCompareOperator::Eq),
1536 "ne" => Some(SelectorCompareOperator::Ne),
1537 "lt" => Some(SelectorCompareOperator::Lt),
1538 "le" => Some(SelectorCompareOperator::Le),
1539 "gt" => Some(SelectorCompareOperator::Gt),
1540 "ge" => Some(SelectorCompareOperator::Ge),
1541 _ => None,
1542 }
1543}
1544
1545#[cfg(debug_assertions)]
1546fn matches_selector_semantics(key: &str, value: &str) -> bool {
1547 matches!(
1548 parse_semantic_param_value(key, value),
1549 SemanticParamValue::Selector(_)
1550 | SemanticParamValue::Reference(_)
1551 | SemanticParamValue::SVarReference(_)
1552 | SemanticParamValue::DelimitedList(_)
1553 | SemanticParamValue::Raw(_)
1554 )
1555}
1556
1557#[cfg(debug_assertions)]
1558fn matches_reference_semantics(key: &str, value: &str) -> bool {
1559 matches!(
1560 parse_semantic_param_value(key, value),
1561 SemanticParamValue::Reference(_)
1562 | SemanticParamValue::Selector(_)
1563 | SemanticParamValue::SVarReference(_)
1564 | SemanticParamValue::Raw(_)
1565 )
1566}
1567
1568#[cfg(debug_assertions)]
1569fn legacy_parse_params(raw: &str) -> BTreeMap<String, String> {
1570 let mut map = BTreeMap::new();
1571 for part in raw.split('|') {
1572 let part = part.trim();
1573 if let Some(idx) = part.find("$ ") {
1574 let key = part[..idx].trim().to_string();
1575 let value = part[idx + 2..].trim().to_string();
1576 map.insert(key, value);
1577 } else if let Some(idx) = part.find('$') {
1578 let key = part[..idx].trim().to_string();
1579 let value = part[idx + 1..].trim().to_string();
1580 map.insert(key, value);
1581 }
1582 }
1583 map
1584}
1585
1586pub fn parse_classified_or_warn<T>(result: Option<T>, kind: &str, raw: &str) -> Option<T> {
1600 if result.is_none() {
1601 let preview: String = raw.trim().chars().take(100).collect();
1602 eprintln!("[parse] failed to parse {kind} from: {preview}");
1603 }
1604 result
1605}
1606
1607pub fn parse_or_warn<T>(result: Option<T>, kind: &str, raw: &str) -> Option<T> {
1608 if result.is_none() {
1609 let trimmed = raw.trim();
1610 let should_warn = match kind {
1611 "StaticAbility" => trimmed.starts_with("S$") || trimmed.starts_with("S:"),
1612 "ReplacementEffect" => trimmed.starts_with("R$") || trimmed.starts_with("R:"),
1613 "Trigger" => trimmed.starts_with("T$") || trimmed.starts_with("T:"),
1614 "ActivatedAbility" => {
1615 trimmed.starts_with("AB$") || trimmed.starts_with("AB:")
1619 }
1620 _ => false,
1621 };
1622 if should_warn {
1623 let preview: String = trimmed.chars().take(100).collect();
1624 eprintln!("[parse] failed to parse {kind} from: {preview}");
1625 }
1626 }
1627 result
1628}
1629
1630impl From<BTreeMap<String, String>> for Params {
1633 fn from(map: BTreeMap<String, String>) -> Self {
1634 Params::from_map(map)
1635 }
1636}
1637
1638impl From<Params> for BTreeMap<String, String> {
1639 fn from(params: Params) -> Self {
1640 params.0
1641 }
1642}
1643
1644pub fn strip_times_multiplier(s: &str) -> (&str, i32) {
1652 if let Some(idx) = s.find("/Times.") {
1653 let mult_str = &s[idx + 7..];
1654 let mult = mult_str.parse::<i32>().unwrap_or(1);
1655 (&s[..idx], mult)
1656 } else {
1657 (s, 1)
1658 }
1659}
1660
1661fn normalize_enchant_type(enchant_type: &str) -> &str {
1666 enchant_type
1667 .split_once(':')
1668 .map(|(kind, _)| kind)
1669 .unwrap_or(enchant_type)
1670 .trim()
1671}
1672
1673pub fn enchant_type_to_valid_tgts(enchant_type: &str) -> String {
1674 let normalized = normalize_enchant_type(enchant_type).trim();
1675 match normalized.to_lowercase().as_str() {
1676 "creature" => "Creature".to_string(),
1677 "land" => "Land".to_string(),
1678 "artifact" => "Artifact".to_string(),
1679 "enchantment" => "Enchantment".to_string(),
1680 "planeswalker" => "Planeswalker".to_string(),
1681 "permanent" | "" => "Permanent".to_string(),
1682 "player" => "Player".to_string(),
1683 "creature or player" => "Creature,Player".to_string(),
1684 _ => normalized.to_string(),
1688 }
1689}
1690
1691pub fn enchant_type_to_target_params(enchant_type: &str) -> String {
1694 let normalized = normalize_enchant_type(enchant_type);
1695 let lower = normalized.to_lowercase();
1696 if lower == "creature.inzonegraveyard" {
1697 return "Origin$ Graveyard | ValidTgts$ Creature".to_string();
1698 }
1699 format!("ValidTgts$ {}", enchant_type_to_valid_tgts(normalized))
1700}
1701
1702pub fn enchant_type_matches_card(
1707 enchant_type: &str,
1708 card: &crate::card::CardInstance,
1709 aura_source: Option<&crate::card::CardInstance>,
1710) -> bool {
1711 let normalized = normalize_enchant_type(enchant_type).trim();
1712 match normalized.to_lowercase().as_str() {
1713 "creature" => card.zone == ZoneType::Battlefield && card.is_creature(),
1714 "creature.inzonegraveyard" => card.zone == ZoneType::Graveyard && card.is_creature(),
1715 "land" => card.zone == ZoneType::Battlefield && card.is_land(),
1716 "artifact" => card.zone == ZoneType::Battlefield && card.type_line.is_artifact(),
1717 "enchantment" => card.zone == ZoneType::Battlefield && card.type_line.is_enchantment(),
1718 "planeswalker" => card.zone == ZoneType::Battlefield && card.type_line.is_planeswalker(),
1719 "permanent" | "" => card.zone == ZoneType::Battlefield,
1720 _ => {
1721 if card.zone != ZoneType::Battlefield {
1729 return false;
1730 }
1731 let source = aura_source.unwrap_or(card);
1732 crate::card::valid_filter::matches_valid_card(normalized, card, source)
1733 }
1734 }
1735}
1736
1737#[cfg(test)]
1738mod tests {
1739 use super::*;
1740
1741 #[test]
1742 fn parse_basic_pipe_params() {
1743 let params = Params::from_raw("Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield");
1744 assert_eq!(params.get("Mode"), Some("ChangesZone"));
1745 assert_eq!(params.get("Origin"), Some("Any"));
1746 assert_eq!(params.get("Destination"), Some("Battlefield"));
1747 }
1748
1749 #[test]
1750 fn parse_bare_dollar_no_space() {
1751 let params = Params::from_raw("Key$Value | Other$ Spaced");
1753 assert_eq!(params.get("Key"), Some("Value"));
1754 assert_eq!(params.get("Other"), Some("Spaced"));
1755 }
1756
1757 #[test]
1758 fn is_true_case_insensitive() {
1759 let params = Params::from_raw("Hidden$ True | Mandatory$ true | Other$ false");
1760 assert!(params.is_true("Hidden"));
1761 assert!(params.is_true("Mandatory"));
1762 assert!(!params.is_true("Other"));
1763 assert!(!params.is_true("Missing"));
1764 }
1765
1766 #[test]
1767 fn numeric_accessors() {
1768 let params = Params::from_raw("Amount$ 3 | Bad$ notanumber");
1769 assert_eq!(params.as_i32("Amount"), Some(3));
1770 assert_eq!(params.as_usize("Amount"), Some(3));
1771 assert_eq!(params.as_i32("Bad"), None);
1772 assert_eq!(params.as_i32("Missing"), None);
1773 }
1774
1775 #[test]
1776 fn get_or_default_works() {
1777 let params = Params::from_raw("Mode$ Continuous");
1778 assert_eq!(params.get_or_default("Mode", "None"), "Continuous");
1779 assert_eq!(params.get_or_default("Missing", "fallback"), "fallback");
1780 }
1781
1782 #[test]
1783 fn serde_roundtrip() {
1784 let params = Params::from_raw("Mode$ Test | Amount$ 5");
1785 let json = serde_json::to_string(¶ms).unwrap();
1786 let deserialized: Params = serde_json::from_str(&json).unwrap();
1787 assert_eq!(deserialized.get("Mode"), Some("Test"));
1788 assert_eq!(deserialized.get("Amount"), Some("5"));
1789 }
1790}