1use std::cmp::Ordering;
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use steel_registry::{
7 REGISTRY, RegistryExt as _, TaggedRegistryExt as _,
8 attribute::{AttributeModifierOperation, AttributeRef},
9 data_component_predicate as registered,
10 data_component_predicate::DataComponentPredicateCodec as _,
11 data_components::{
12 Component, ComponentData, ComponentEntry, DataComponentType, PotionContents,
13 vanilla_components,
14 },
15 enchantment::EnchantmentRef,
16 equipment::EquipmentSlotGroup,
17 item_predicate::{DoubleBounds, IntBounds, ItemPredicate as RegisteredItemPredicate},
18 item_stack::ItemStack,
19 items::ItemRef,
20};
21use steel_utils::DowncastType;
22use steel_utils::{Identifier, nbt::parse_snbt_argument};
23use text_components::TextComponent;
24
25use crate::command::brigadier::{
26 CommandSyntaxError, CommandSyntaxErrorKind, ReaderCursor, StringReader, SuggestionsBuilder,
27};
28
29use super::{
30 argument::parse_identifier,
31 item::{component_value_is_valid, numeric_i32, read_component_value},
32 suggestions::matches_substring,
33};
34
35const VANILLA_DATA_COMPONENT_PREDICATE_KEYS: &[&str] = &[
36 "damage",
37 "enchantments",
38 "stored_enchantments",
39 "potion_contents",
40 "custom_data",
41 "container",
42 "bundle_contents",
43 "firework_explosion",
44 "fireworks",
45 "writable_book_content",
46 "written_book_content",
47 "attribute_modifiers",
48 "trim",
49 "jukebox_playable",
50 "villager/variant",
51];
52
53#[derive(Clone, Debug, PartialEq)]
55pub(crate) struct ItemPredicate {
56 target: ItemPredicateTarget,
57 conditions: Vec<ItemPredicateCondition>,
58}
59
60#[derive(Clone, Debug, PartialEq)]
61enum ItemPredicateTarget {
62 Any,
63 Item(ItemRef),
64 Tag(Identifier),
65}
66
67#[derive(Clone, Debug, PartialEq)]
68struct ItemPredicateCondition {
69 alternatives: Vec<ItemPredicateTerm>,
70}
71
72#[derive(Clone, Debug, PartialEq)]
73enum ItemPredicateTerm {
74 Always,
75 ComponentPresence(Identifier),
76 ComponentValue {
77 key: Identifier,
78 value: Box<ComponentData>,
79 },
80 Count(IntRange),
81 DataPredicate(DataComponentPredicate),
82 Not(Box<Self>),
83}
84
85#[derive(Clone, Debug, PartialEq)]
86enum DataComponentPredicate {
87 CustomData(vanilla_components::CustomData),
88 Damage(DamagePredicate),
89 Enchantments {
90 stored: bool,
91 predicates: Vec<EnchantmentPredicate>,
92 },
93 AttributeModifiers(AttributeModifiersPredicate),
94 Potions(registered::PotionsPredicate),
95 Container(registered::ContainerPredicate),
96 Bundle(registered::BundlePredicate),
97 FireworkExplosion(registered::FireworkExplosionPredicate),
98 Fireworks(registered::FireworksPredicate),
99 WritableBook(registered::WritableBookPredicate),
100 WrittenBook(registered::WrittenBookPredicate),
101 Trim(registered::TrimPredicate),
102 JukeboxPlayable(registered::JukeboxPlayablePredicate),
103 VillagerType(registered::VillagerTypePredicate),
104}
105
106trait ItemInstanceView {
107 fn item_ref(&self) -> ItemRef;
108 fn item_count(&self) -> i32;
109 fn effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData>;
110
111 fn component<T: Component + DowncastType>(
112 &self,
113 component: DataComponentType<T>,
114 ) -> Option<&T> {
115 self.effective_value_raw(component.key())
116 .and_then(ComponentData::downcast_ref::<T>)
117 }
118}
119
120impl ItemInstanceView for ItemStack {
121 fn item_ref(&self) -> ItemRef {
122 self.item()
123 }
124
125 fn item_count(&self) -> i32 {
126 self.count()
127 }
128
129 fn effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
130 self.get_effective_value_raw(key)
131 }
132}
133
134impl ItemInstanceView for steel_registry::ItemStackTemplate {
135 fn item_ref(&self) -> ItemRef {
136 self.item()
137 }
138
139 fn item_count(&self) -> i32 {
140 self.count()
141 }
142
143 fn effective_value_raw(&self, key: &Identifier) -> Option<&ComponentData> {
144 self.get_effective_value_raw(key)
145 }
146}
147
148impl ItemPredicate {
149 #[must_use]
150 pub(crate) fn matches(&self, stack: &ItemStack) -> bool {
151 self.target.matches(stack)
152 && self
153 .conditions
154 .iter()
155 .all(|condition| condition.matches(stack))
156 }
157}
158
159impl ItemPredicateTarget {
160 fn matches(&self, stack: &ItemStack) -> bool {
161 match self {
162 Self::Any => true,
163 Self::Item(item) => stack.is(item),
164 Self::Tag(tag) => REGISTRY.items.is_in_tag(stack.item(), tag),
165 }
166 }
167}
168
169impl ItemPredicateCondition {
170 fn matches(&self, stack: &ItemStack) -> bool {
171 self.alternatives
172 .iter()
173 .any(|alternative| alternative.matches(stack))
174 }
175}
176
177impl ItemPredicateTerm {
178 fn matches(&self, stack: &ItemStack) -> bool {
179 match self {
180 Self::Always => true,
181 Self::ComponentPresence(key) => stack.has_component(key),
182 Self::ComponentValue { key, value } => stack
183 .get_effective_value_raw(key)
184 .is_some_and(|actual| actual == value.as_ref()),
185 Self::Count(range) => range.matches(stack.count()),
186 Self::DataPredicate(predicate) => predicate.matches(stack),
187 Self::Not(term) => !term.matches(stack),
188 }
189 }
190}
191
192impl DataComponentPredicate {
193 #[expect(
194 clippy::too_many_lines,
195 reason = "keeping every registered predicate variant in one match makes coverage auditable"
196 )]
197 fn matches<S: ItemInstanceView + ?Sized>(&self, stack: &S) -> bool {
198 match self {
199 Self::CustomData(expected) => stack
200 .component(vanilla_components::CUSTOM_DATA)
201 .cloned()
202 .unwrap_or_default()
203 .matched_by(expected.as_compound()),
204 Self::Damage(predicate) => predicate.matches(stack),
205 Self::Enchantments { stored, predicates } => {
206 let enchantments = if *stored {
207 stack.component(vanilla_components::STORED_ENCHANTMENTS)
208 } else {
209 stack.component(vanilla_components::ENCHANTMENTS)
210 };
211 enchantments.is_some_and(|enchantments| {
212 predicates
213 .iter()
214 .all(|predicate| predicate.matches(enchantments))
215 })
216 }
217 Self::AttributeModifiers(predicate) => stack
218 .component(vanilla_components::ATTRIBUTE_MODIFIERS)
219 .is_some_and(|modifiers| predicate.matches(modifiers)),
220 Self::Potions(predicate) => stack
221 .component(vanilla_components::POTION_CONTENTS)
222 .and_then(PotionContents::potion)
223 .is_some_and(|potion| predicate.potions().contains(potion.value())),
224 Self::Container(predicate) => stack
225 .component(vanilla_components::CONTAINER)
226 .is_some_and(|contents| {
227 predicate.items().is_none_or(|items| {
228 collection_matches(
229 items,
230 contents.items().iter().filter_map(Option::as_ref),
231 registered_item_predicate_matches,
232 )
233 })
234 }),
235 Self::Bundle(predicate) => stack
236 .component(vanilla_components::BUNDLE_CONTENTS)
237 .is_some_and(|contents| {
238 predicate.items().is_none_or(|items| {
239 collection_matches(
240 items,
241 contents.items().iter(),
242 registered_item_predicate_matches,
243 )
244 })
245 }),
246 Self::FireworkExplosion(predicate) => stack
247 .component(vanilla_components::FIREWORK_EXPLOSION)
248 .is_some_and(|explosion| firework_matches(predicate.predicate(), explosion)),
249 Self::Fireworks(predicate) => stack
250 .component(vanilla_components::FIREWORKS)
251 .is_some_and(|fireworks| {
252 int_bounds_matches(predicate.flight_duration(), fireworks.flight_duration())
253 && predicate.explosions().is_none_or(|explosions| {
254 collection_matches(
255 explosions,
256 fireworks.explosions().iter(),
257 firework_matches,
258 )
259 })
260 }),
261 Self::WritableBook(predicate) => stack
262 .component(vanilla_components::WRITABLE_BOOK_CONTENT)
263 .is_some_and(|book| {
264 predicate.pages().is_none_or(|pages| {
265 collection_matches(pages, book.pages().iter(), |predicate, page| {
266 predicate.contents() == page.raw()
267 })
268 })
269 }),
270 Self::WrittenBook(predicate) => stack
271 .component(vanilla_components::WRITTEN_BOOK_CONTENT)
272 .is_some_and(|book| {
273 predicate
274 .author()
275 .is_none_or(|author| author == book.author())
276 && predicate
277 .title()
278 .is_none_or(|title| title == book.title().raw())
279 && int_bounds_matches(predicate.generation(), book.generation())
280 && predicate
281 .resolved()
282 .is_none_or(|resolved| resolved == book.resolved())
283 && predicate.pages().is_none_or(|pages| {
284 collection_matches(pages, book.pages().iter(), |predicate, page| {
285 predicate.contents() == page.raw()
286 })
287 })
288 }),
289 Self::Trim(predicate) => {
290 stack
291 .component(vanilla_components::TRIM)
292 .is_some_and(|trim| {
293 predicate.material().is_none_or(|materials| {
294 trim.material()
295 .as_reference()
296 .is_some_and(|material| materials.contains(material))
297 }) && predicate.pattern().is_none_or(|patterns| {
298 trim.pattern()
299 .as_reference()
300 .is_some_and(|pattern| patterns.contains(pattern))
301 })
302 })
303 }
304 Self::JukeboxPlayable(predicate) => stack
305 .component(vanilla_components::JUKEBOX_PLAYABLE)
306 .is_some_and(|playable| {
307 predicate.song().is_none_or(|songs| {
308 playable
309 .song()
310 .as_reference()
311 .is_some_and(|song| songs.contains(song))
312 })
313 }),
314 Self::VillagerType(predicate) => stack
315 .component(vanilla_components::VILLAGER_VARIANT)
316 .is_some_and(|villager_type| {
317 predicate.villager_types().contains(villager_type.value())
318 }),
319 }
320 }
321}
322
323fn int_bounds_matches(bounds: IntBounds, value: i32) -> bool {
324 bounds.min().is_none_or(|minimum| value >= minimum)
325 && bounds.max().is_none_or(|maximum| value <= maximum)
326}
327
328fn double_bounds_matches(bounds: DoubleBounds, value: f64) -> bool {
329 bounds.min().is_none_or(|minimum| value >= minimum)
330 && bounds.max().is_none_or(|maximum| value <= maximum)
331}
332
333fn collection_matches<'a, P, T: 'a>(
334 predicate: ®istered::CollectionPredicate<P>,
335 values: impl IntoIterator<Item = &'a T>,
336 matches: impl Fn(&P, &T) -> bool + Copy,
337) -> bool {
338 let values = values.into_iter().collect::<Vec<_>>();
339 predicate.contains().is_none_or(|predicates| {
340 predicates
341 .iter()
342 .all(|predicate| values.iter().any(|value| matches(predicate, value)))
343 }) && predicate.counts().is_none_or(|predicates| {
344 predicates.iter().all(|predicate| {
345 let count = values
346 .iter()
347 .filter(|value| matches(predicate.test(), value))
348 .count();
349 i32::try_from(count).is_ok_and(|count| int_bounds_matches(predicate.count(), count))
350 })
351 }) && predicate.size().is_none_or(|size| {
352 i32::try_from(values.len()).is_ok_and(|length| int_bounds_matches(*size, length))
353 })
354}
355
356fn registered_item_predicate_matches(
357 predicate: &RegisteredItemPredicate,
358 template: &steel_registry::ItemStackTemplate,
359) -> bool {
360 predicate
361 .items()
362 .is_none_or(|items| items.contains(template.item_ref()))
363 && int_bounds_matches(predicate.count(), template.item_count())
364 && predicate
365 .components()
366 .exact()
367 .values()
368 .iter()
369 .all(|(entry, expected)| template.effective_value_raw(&entry.key) == Some(expected))
370 && predicate
371 .components()
372 .partial()
373 .iter()
374 .all(|predicate| registered_partial_matches(predicate, template))
375}
376
377fn registered_partial_matches<S: ItemInstanceView + ?Sized>(
378 predicate: ®istered::DataComponentPredicateData,
379 stack: &S,
380) -> bool {
381 if let Some(component) = predicate.any_component() {
382 return stack.effective_value_raw(&component.key).is_some();
383 }
384 macro_rules! match_registered {
385 ($type:ty, $variant:ident) => {
386 if let Some(value) = predicate.downcast_ref::<$type>() {
387 return DataComponentPredicate::$variant(value.clone()).matches(stack);
388 }
389 };
390 }
391 match_registered!(registered::PotionsPredicate, Potions);
392 match_registered!(registered::ContainerPredicate, Container);
393 match_registered!(registered::BundlePredicate, Bundle);
394 match_registered!(registered::FireworkExplosionPredicate, FireworkExplosion);
395 match_registered!(registered::FireworksPredicate, Fireworks);
396 match_registered!(registered::WritableBookPredicate, WritableBook);
397 match_registered!(registered::WrittenBookPredicate, WrittenBook);
398 match_registered!(registered::TrimPredicate, Trim);
399 match_registered!(registered::JukeboxPlayablePredicate, JukeboxPlayable);
400 match_registered!(registered::VillagerTypePredicate, VillagerType);
401
402 if let Some(value) = predicate.downcast_ref::<registered::CustomDataPredicate>() {
403 return stack
404 .component(vanilla_components::CUSTOM_DATA)
405 .cloned()
406 .unwrap_or_default()
407 .matched_by(value.value().tag());
408 }
409 if let Some(value) = predicate.downcast_ref::<registered::DamagePredicate>() {
410 let Some(damage) = stack.component(vanilla_components::DAMAGE).copied() else {
411 return false;
412 };
413 let maximum = stack
414 .component(vanilla_components::MAX_DAMAGE)
415 .copied()
416 .unwrap_or(0);
417 return int_bounds_matches(value.durability(), maximum - damage)
418 && int_bounds_matches(value.damage(), damage);
419 }
420 if let Some(value) = predicate.downcast_ref::<registered::EnchantmentsPredicate>() {
421 return stack
422 .component(vanilla_components::ENCHANTMENTS)
423 .is_some_and(|enchantments| {
424 value
425 .enchantments()
426 .iter()
427 .all(|predicate| registered_enchantment_matches(predicate, enchantments))
428 });
429 }
430 if let Some(value) = predicate.downcast_ref::<registered::StoredEnchantmentsPredicate>() {
431 return stack
432 .component(vanilla_components::STORED_ENCHANTMENTS)
433 .is_some_and(|enchantments| {
434 value
435 .enchantments()
436 .iter()
437 .all(|predicate| registered_enchantment_matches(predicate, enchantments))
438 });
439 }
440 if let Some(value) = predicate.downcast_ref::<registered::AttributeModifiersPredicate>() {
441 return stack
442 .component(vanilla_components::ATTRIBUTE_MODIFIERS)
443 .is_some_and(|modifiers| {
444 value.modifiers().is_none_or(|predicate| {
445 collection_matches(
446 predicate,
447 modifiers.modifiers.iter(),
448 registered_attribute_modifier_matches,
449 )
450 })
451 });
452 }
453 false
454}
455
456fn registered_enchantment_matches(
457 predicate: ®istered::EnchantmentPredicate,
458 enchantments: &vanilla_components::ItemEnchantments,
459) -> bool {
460 enchantments.iter().any(|(key, level)| {
461 predicate.enchantments().is_none_or(|expected| {
462 REGISTRY
463 .enchantments
464 .by_key(key)
465 .is_some_and(|enchantment| expected.contains(enchantment))
466 }) && predicate
467 .levels()
468 .min()
469 .is_none_or(|minimum| i64::from(*level) >= i64::from(minimum))
470 && predicate
471 .levels()
472 .max()
473 .is_none_or(|maximum| i64::from(*level) <= i64::from(maximum))
474 })
475}
476
477fn registered_attribute_modifier_matches(
478 predicate: ®istered::AttributeModifierEntryPredicate,
479 modifier: &vanilla_components::ItemAttributeModifierEntry,
480) -> bool {
481 predicate
482 .attribute()
483 .is_none_or(|attributes| attributes.contains(modifier.attribute))
484 && predicate.id().is_none_or(|id| id == &modifier.id)
485 && double_bounds_matches(predicate.amount(), modifier.amount)
486 && predicate
487 .operation()
488 .is_none_or(|operation| operation == modifier.operation)
489 && predicate.slot().is_none_or(|slot| slot == modifier.slot)
490}
491
492fn firework_matches(
493 predicate: ®istered::FireworkPredicate,
494 explosion: &vanilla_components::FireworkExplosion,
495) -> bool {
496 predicate
497 .shape()
498 .is_none_or(|shape| shape == explosion.shape())
499 && predicate
500 .has_twinkle()
501 .is_none_or(|twinkle| twinkle == explosion.has_twinkle())
502 && predicate
503 .has_trail()
504 .is_none_or(|trail| trail == explosion.has_trail())
505}
506
507pub(super) fn parse_item_predicate(
508 reader: &mut StringReader<'_>,
509) -> Result<ItemPredicate, CommandSyntaxError> {
510 let start = reader.checkpoint();
511 let result = parse_item_predicate_inner(reader);
512 if result.is_err() {
513 reader.restore(start);
514 }
515 result
516}
517
518fn parse_item_predicate_inner(
519 reader: &mut StringReader<'_>,
520) -> Result<ItemPredicate, CommandSyntaxError> {
521 let target = parse_target(reader)?;
522 let before_whitespace = reader.checkpoint();
523 reader.skip_whitespace();
524 if reader.peek() != Some('[') {
525 reader.restore(before_whitespace);
526 return Ok(ItemPredicate {
527 target,
528 conditions: Vec::new(),
529 });
530 }
531
532 reader.skip();
533 reader.skip_whitespace();
534 if reader.peek() == Some(']') {
535 reader.skip();
536 return Ok(ItemPredicate {
537 target,
538 conditions: Vec::new(),
539 });
540 }
541
542 let mut conditions = Vec::new();
543 loop {
544 conditions.push(parse_condition(reader)?);
545 reader.skip_whitespace();
546 if reader.peek() != Some(',') {
547 reader.expect(']')?;
548 break;
549 }
550 reader.skip();
551 }
552
553 Ok(ItemPredicate { target, conditions })
554}
555
556fn parse_target(reader: &mut StringReader<'_>) -> Result<ItemPredicateTarget, CommandSyntaxError> {
557 reader.skip_whitespace();
558 if reader.peek() == Some('*') {
559 reader.skip();
560 return Ok(ItemPredicateTarget::Any);
561 }
562
563 if reader.peek() == Some('#') {
564 let start = reader.checkpoint();
565 reader.skip();
566 let key = parse_identifier(reader)?;
567 if REGISTRY.items.get_tag(&key).is_some() {
568 return Ok(ItemPredicateTarget::Tag(key));
569 }
570 reader.restore(start);
571 return Err(dynamic_error(reader, format!("Unknown item tag '#{key}'")));
572 }
573
574 let start = reader.checkpoint();
575 let key = parse_identifier(reader)?;
576 let Some(item) = REGISTRY.items.by_key(&key) else {
577 reader.restore(start);
578 return Err(dynamic_error(reader, format!("Unknown item '{key}'")));
579 };
580 Ok(ItemPredicateTarget::Item(item))
581}
582
583fn parse_condition(
584 reader: &mut StringReader<'_>,
585) -> Result<ItemPredicateCondition, CommandSyntaxError> {
586 let mut alternatives = Vec::new();
587 loop {
588 alternatives.push(parse_term(reader)?);
589 reader.skip_whitespace();
590 if reader.peek() != Some('|') {
591 break;
592 }
593 reader.skip();
594 }
595 Ok(ItemPredicateCondition { alternatives })
596}
597
598fn parse_term(reader: &mut StringReader<'_>) -> Result<ItemPredicateTerm, CommandSyntaxError> {
599 reader.skip_whitespace();
600 if reader.peek() == Some('!') {
601 reader.skip();
602 return parse_test(reader).map(|term| ItemPredicateTerm::Not(Box::new(term)));
603 }
604 parse_test(reader)
605}
606
607fn parse_test(reader: &mut StringReader<'_>) -> Result<ItemPredicateTerm, CommandSyntaxError> {
608 reader.skip_whitespace();
609 let key_start = reader.checkpoint();
610 let key = parse_identifier(reader)?;
611 reader.skip_whitespace();
612
613 match reader.peek() {
614 Some('=') => {
615 reader.skip();
616 parse_component_value_test(reader, key_start, key)
617 }
618 Some('~') => {
619 reader.skip();
620 parse_predicate_value_test(reader, key_start, key)
621 }
622 _ => parse_component_presence_test(reader, key_start, key),
623 }
624}
625
626fn parse_component_presence_test(
627 reader: &mut StringReader<'_>,
628 key_start: ReaderCursor,
629 key: Identifier,
630) -> Result<ItemPredicateTerm, CommandSyntaxError> {
631 if is_count_key(&key) {
632 return Ok(ItemPredicateTerm::Always);
633 }
634 if persistent_component(&key).is_some() {
635 return Ok(ItemPredicateTerm::ComponentPresence(key));
636 }
637 reader.restore(key_start);
638 Err(dynamic_error(
639 reader,
640 format!("Unknown item component '{key}'"),
641 ))
642}
643
644fn parse_component_value_test(
645 reader: &mut StringReader<'_>,
646 key_start: ReaderCursor,
647 key: Identifier,
648) -> Result<ItemPredicateTerm, CommandSyntaxError> {
649 let tag = read_nbt(reader, "component", &key)?;
650 if is_count_key(&key) {
651 return parse_int_range(&tag)
652 .map(ItemPredicateTerm::Count)
653 .ok_or_else(|| malformed_component(reader, &key));
654 }
655
656 let Some(entry) = persistent_component(&key) else {
657 reader.restore(key_start);
658 return Err(dynamic_error(
659 reader,
660 format!("Unknown item component '{key}'"),
661 ));
662 };
663 let Some(value) = read_component_value(entry, &tag) else {
664 return Err(malformed_component(reader, &key));
665 };
666 if !component_value_is_valid(&key, &value) {
667 return Err(malformed_component(reader, &key));
668 }
669 Ok(ItemPredicateTerm::ComponentValue {
670 key,
671 value: Box::new(value),
672 })
673}
674
675fn parse_predicate_value_test(
676 reader: &mut StringReader<'_>,
677 key_start: ReaderCursor,
678 key: Identifier,
679) -> Result<ItemPredicateTerm, CommandSyntaxError> {
680 let tag = read_nbt(reader, "predicate", &key)?;
681 if is_count_key(&key) {
682 return parse_int_range(&tag)
683 .map(ItemPredicateTerm::Count)
684 .ok_or_else(|| malformed_predicate(reader, &key));
685 }
686
687 if is_vanilla_predicate_key(&key) {
688 let predicate = parse_supported_data_predicate(&key, &tag)
689 .ok_or_else(|| malformed_predicate(reader, &key))?;
690 return Ok(ItemPredicateTerm::DataPredicate(predicate));
691 }
692
693 if REGISTRY.data_components.by_key(&key).is_some() {
694 if !matches!(tag, NbtTag::Compound(_)) {
695 return Err(malformed_predicate(reader, &key));
696 }
697 return Ok(ItemPredicateTerm::ComponentPresence(key));
698 }
699
700 reader.restore(key_start);
701 Err(dynamic_error(
702 reader,
703 format!("Unknown item predicate '{key}'"),
704 ))
705}
706
707fn read_nbt(
708 reader: &mut StringReader<'_>,
709 description: &str,
710 key: &Identifier,
711) -> Result<NbtTag, CommandSyntaxError> {
712 reader.skip_whitespace();
713 let (tag, consumed) = parse_snbt_argument(reader.remaining()).map_err(|error| {
714 reader.advance_bytes(error.cursor());
715 dynamic_error(reader, error.component())
716 })?;
717 if !reader.advance_bytes(consumed) {
718 return Err(dynamic_error(
719 reader,
720 format!("Malformed item {description} '{key}'"),
721 ));
722 }
723 Ok(tag)
724}
725
726fn persistent_component(key: &Identifier) -> Option<&'static ComponentEntry> {
727 let entry = REGISTRY.data_components.by_key(key)?;
728 entry.is_persistent().then_some(entry)
729}
730
731fn malformed_component(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
732 dynamic_error(reader, format!("Malformed item component '{key}'"))
733}
734
735fn malformed_predicate(reader: &StringReader<'_>, key: &Identifier) -> CommandSyntaxError {
736 dynamic_error(reader, format!("Malformed item predicate '{key}'"))
737}
738
739fn dynamic_error(
740 reader: &StringReader<'_>,
741 message: impl Into<TextComponent>,
742) -> CommandSyntaxError {
743 reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message.into())))
744}
745
746fn is_count_key(key: &Identifier) -> bool {
747 key.namespace == Identifier::VANILLA_NAMESPACE && key.path == "count"
748}
749
750fn is_vanilla_predicate_key(key: &Identifier) -> bool {
751 key.namespace == Identifier::VANILLA_NAMESPACE
752 && VANILLA_DATA_COMPONENT_PREDICATE_KEYS
753 .iter()
754 .any(|path| key.path == *path)
755}
756
757fn parse_supported_data_predicate(
758 key: &Identifier,
759 tag: &NbtTag,
760) -> Option<DataComponentPredicate> {
761 match key.path.as_ref() {
762 "custom_data" => vanilla_components::CustomData::from_nbt_value(tag)
763 .map(DataComponentPredicate::CustomData),
764 "damage" => parse_damage_predicate(tag).map(DataComponentPredicate::Damage),
765 "enchantments" => parse_enchantment_predicates(tag).map(|predicates| {
766 DataComponentPredicate::Enchantments {
767 stored: false,
768 predicates,
769 }
770 }),
771 "stored_enchantments" => parse_enchantment_predicates(tag).map(|predicates| {
772 DataComponentPredicate::Enchantments {
773 stored: true,
774 predicates,
775 }
776 }),
777 "attribute_modifiers" => {
778 parse_attribute_modifiers_predicate(tag).map(DataComponentPredicate::AttributeModifiers)
779 }
780 "potion_contents" => {
781 registered::PotionsPredicate::from_nbt_value(tag).map(DataComponentPredicate::Potions)
782 }
783 "container" => registered::ContainerPredicate::from_nbt_value(tag)
784 .map(DataComponentPredicate::Container),
785 "bundle_contents" => {
786 registered::BundlePredicate::from_nbt_value(tag).map(DataComponentPredicate::Bundle)
787 }
788 "firework_explosion" => registered::FireworkExplosionPredicate::from_nbt_value(tag)
789 .map(DataComponentPredicate::FireworkExplosion),
790 "fireworks" => registered::FireworksPredicate::from_nbt_value(tag)
791 .map(DataComponentPredicate::Fireworks),
792 "writable_book_content" => registered::WritableBookPredicate::from_nbt_value(tag)
793 .map(DataComponentPredicate::WritableBook),
794 "written_book_content" => registered::WrittenBookPredicate::from_nbt_value(tag)
795 .map(DataComponentPredicate::WrittenBook),
796 "trim" => registered::TrimPredicate::from_nbt_value(tag).map(DataComponentPredicate::Trim),
797 "jukebox_playable" => registered::JukeboxPlayablePredicate::from_nbt_value(tag)
798 .map(DataComponentPredicate::JukeboxPlayable),
799 "villager/variant" => registered::VillagerTypePredicate::from_nbt_value(tag)
800 .map(DataComponentPredicate::VillagerType),
801 _ => None,
802 }
803}
804
805#[derive(Clone, Copy, Debug, PartialEq)]
806struct IntRange {
807 min: Option<i32>,
808 max: Option<i32>,
809}
810
811impl IntRange {
812 const ANY: Self = Self {
813 min: None,
814 max: None,
815 };
816
817 fn matches(self, value: i32) -> bool {
818 self.min.is_none_or(|minimum| value >= minimum)
819 && self.max.is_none_or(|maximum| value <= maximum)
820 }
821
822 fn matches_u32(self, value: u32) -> bool {
823 let value = i64::from(value);
824 self.min.is_none_or(|minimum| value >= i64::from(minimum))
825 && self.max.is_none_or(|maximum| value <= i64::from(maximum))
826 }
827
828 fn matches_usize(self, value: usize) -> bool {
829 let minimum_matches = self.min.is_none_or(|minimum| {
830 minimum <= 0 || usize::try_from(minimum).is_ok_and(|minimum| value >= minimum)
831 });
832 let maximum_matches = self
833 .max
834 .is_none_or(|maximum| usize::try_from(maximum).is_ok_and(|maximum| value <= maximum));
835 minimum_matches && maximum_matches
836 }
837
838 const fn is_any(self) -> bool {
839 self.min.is_none() && self.max.is_none()
840 }
841}
842
843fn parse_int_range(tag: &NbtTag) -> Option<IntRange> {
844 if let Some(value) = numeric_i32(tag) {
845 return Some(IntRange {
846 min: Some(value),
847 max: Some(value),
848 });
849 }
850 let NbtTag::Compound(compound) = tag else {
851 return None;
852 };
853 let min = parse_optional_field(compound, "min", numeric_i32).ok()?;
854 let max = parse_optional_field(compound, "max", numeric_i32).ok()?;
855 if min.zip(max).is_some_and(|(min, max)| min > max) {
856 return None;
857 }
858 Some(IntRange { min, max })
859}
860
861#[derive(Clone, Copy, Debug, PartialEq)]
862struct DamagePredicate {
863 durability: IntRange,
864 damage: IntRange,
865}
866
867impl DamagePredicate {
868 fn matches<S: ItemInstanceView + ?Sized>(self, stack: &S) -> bool {
869 let Some(damage) = stack.component(vanilla_components::DAMAGE).copied() else {
870 return false;
871 };
872 let maximum = stack
873 .component(vanilla_components::MAX_DAMAGE)
874 .copied()
875 .unwrap_or(0);
876 self.durability.matches(maximum - damage) && self.damage.matches(damage)
877 }
878}
879
880fn parse_damage_predicate(tag: &NbtTag) -> Option<DamagePredicate> {
881 let NbtTag::Compound(compound) = tag else {
882 return None;
883 };
884 let durability = parse_optional_field(compound, "durability", parse_int_range)
885 .ok()?
886 .unwrap_or(IntRange::ANY);
887 let damage = parse_optional_field(compound, "damage", parse_int_range)
888 .ok()?
889 .unwrap_or(IntRange::ANY);
890 Some(DamagePredicate { durability, damage })
891}
892
893#[derive(Clone, Debug, PartialEq)]
894struct EnchantmentPredicate {
895 enchantments: Option<Vec<EnchantmentRef>>,
896 levels: IntRange,
897}
898
899impl EnchantmentPredicate {
900 fn matches(&self, enchantments: &vanilla_components::ItemEnchantments) -> bool {
901 if let Some(expected) = &self.enchantments {
902 return expected.iter().any(|enchantment| {
903 let level = enchantments.get_level(&enchantment.key);
904 level != 0 && self.levels.matches_u32(level)
905 });
906 }
907 if !self.levels.is_any() {
908 return enchantments
909 .iter()
910 .any(|(_, level)| self.levels.matches_u32(*level));
911 }
912 !enchantments.is_empty()
913 }
914}
915
916fn parse_enchantment_predicates(tag: &NbtTag) -> Option<Vec<EnchantmentPredicate>> {
917 match tag {
918 NbtTag::List(NbtList::Empty) => Some(Vec::new()),
919 NbtTag::List(NbtList::Compound(compounds)) => {
920 compounds.iter().map(parse_enchantment_predicate).collect()
921 }
922 _ => None,
923 }
924}
925
926fn parse_enchantment_predicate(compound: &NbtCompound) -> Option<EnchantmentPredicate> {
927 let enchantments =
928 parse_optional_field(compound, "enchantments", parse_enchantment_holder_set).ok()?;
929 let levels = parse_optional_field(compound, "levels", parse_int_range)
930 .ok()?
931 .unwrap_or(IntRange::ANY);
932 Some(EnchantmentPredicate {
933 enchantments,
934 levels,
935 })
936}
937
938fn parse_enchantment_holder_set(tag: &NbtTag) -> Option<Vec<EnchantmentRef>> {
939 match tag {
940 NbtTag::String(value) => parse_enchantment_holder(&value.to_string()),
941 NbtTag::List(NbtList::Empty) => Some(Vec::new()),
942 NbtTag::List(NbtList::String(values)) => {
943 let mut enchantments = Vec::new();
944 for value in values {
945 enchantments.push(parse_enchantment_reference(&value.to_string())?);
946 }
947 Some(enchantments)
948 }
949 _ => None,
950 }
951}
952
953fn parse_enchantment_holder(value: &str) -> Option<Vec<EnchantmentRef>> {
954 if let Some(tag) = value.strip_prefix('#') {
955 let key = parse_identifier_with_default_namespace(tag)?;
956 return REGISTRY.enchantments.get_tag(&key);
957 }
958 parse_enchantment_reference(value).map(|enchantment| vec![enchantment])
959}
960
961fn parse_enchantment_reference(value: &str) -> Option<EnchantmentRef> {
962 let key = parse_identifier_with_default_namespace(value)?;
963 REGISTRY.enchantments.by_key(&key)
964}
965
966#[derive(Clone, Debug, PartialEq)]
967struct AttributeModifiersPredicate {
968 modifiers: Option<AttributeModifierCollectionPredicate>,
969}
970
971impl AttributeModifiersPredicate {
972 fn matches(&self, modifiers: &vanilla_components::ItemAttributeModifiers) -> bool {
973 self.modifiers
974 .as_ref()
975 .is_none_or(|predicate| predicate.matches(&modifiers.modifiers))
976 }
977}
978
979fn parse_attribute_modifiers_predicate(tag: &NbtTag) -> Option<AttributeModifiersPredicate> {
980 let NbtTag::Compound(compound) = tag else {
981 return None;
982 };
983 let modifiers =
984 parse_optional_field(compound, "modifiers", parse_attribute_modifier_collection).ok()?;
985 Some(AttributeModifiersPredicate { modifiers })
986}
987
988#[derive(Clone, Debug, PartialEq)]
989struct AttributeModifierCollectionPredicate {
990 contains: Vec<AttributeModifierEntryPredicate>,
991 counts: Vec<AttributeModifierCountPredicate>,
992 size: Option<IntRange>,
993}
994
995impl AttributeModifierCollectionPredicate {
996 fn matches(&self, modifiers: &[vanilla_components::ItemAttributeModifierEntry]) -> bool {
997 let mut matched = vec![false; self.contains.len()];
998 for modifier in modifiers {
999 for (matched, predicate) in matched.iter_mut().zip(&self.contains) {
1000 if !*matched && predicate.matches(modifier) {
1001 *matched = true;
1002 }
1003 }
1004 }
1005 matched.into_iter().all(|matched| matched)
1006 && self.counts.iter().all(|predicate| {
1007 let count = modifiers
1008 .iter()
1009 .filter(|modifier| predicate.test.matches(modifier))
1010 .count();
1011 predicate.count.matches_usize(count)
1012 })
1013 && self
1014 .size
1015 .is_none_or(|range| range.matches_usize(modifiers.len()))
1016 }
1017}
1018
1019fn parse_attribute_modifier_collection(
1020 tag: &NbtTag,
1021) -> Option<AttributeModifierCollectionPredicate> {
1022 let NbtTag::Compound(compound) = tag else {
1023 return None;
1024 };
1025 let contains = parse_optional_field(
1026 compound,
1027 "contains",
1028 parse_attribute_modifier_predicate_list,
1029 )
1030 .ok()?
1031 .unwrap_or_default();
1032 let counts = parse_optional_field(compound, "count", parse_attribute_modifier_count_list)
1033 .ok()?
1034 .unwrap_or_default();
1035 let size = parse_optional_field(compound, "size", parse_int_range).ok()?;
1036 Some(AttributeModifierCollectionPredicate {
1037 contains,
1038 counts,
1039 size,
1040 })
1041}
1042
1043fn parse_attribute_modifier_predicate_list(
1044 tag: &NbtTag,
1045) -> Option<Vec<AttributeModifierEntryPredicate>> {
1046 match tag {
1047 NbtTag::List(NbtList::Empty) => Some(Vec::new()),
1048 NbtTag::List(NbtList::Compound(compounds)) => compounds
1049 .iter()
1050 .map(parse_attribute_modifier_entry)
1051 .collect(),
1052 _ => None,
1053 }
1054}
1055
1056fn parse_attribute_modifier_count_list(
1057 tag: &NbtTag,
1058) -> Option<Vec<AttributeModifierCountPredicate>> {
1059 match tag {
1060 NbtTag::List(NbtList::Empty) => Some(Vec::new()),
1061 NbtTag::List(NbtList::Compound(compounds)) => compounds
1062 .iter()
1063 .map(parse_attribute_modifier_count)
1064 .collect(),
1065 _ => None,
1066 }
1067}
1068
1069#[derive(Clone, Debug, PartialEq)]
1070struct AttributeModifierCountPredicate {
1071 test: AttributeModifierEntryPredicate,
1072 count: IntRange,
1073}
1074
1075fn parse_attribute_modifier_count(
1076 compound: &NbtCompound,
1077) -> Option<AttributeModifierCountPredicate> {
1078 let NbtTag::Compound(test) = compound.get("test")? else {
1079 return None;
1080 };
1081 let count = parse_int_range(compound.get("count")?)?;
1082 Some(AttributeModifierCountPredicate {
1083 test: parse_attribute_modifier_entry(test)?,
1084 count,
1085 })
1086}
1087
1088#[derive(Clone, Debug, PartialEq)]
1089struct AttributeModifierEntryPredicate {
1090 attributes: Option<Vec<AttributeRef>>,
1091 id: Option<Identifier>,
1092 amount: DoubleRange,
1093 operation: Option<AttributeModifierOperation>,
1094 slot: Option<EquipmentSlotGroup>,
1095}
1096
1097impl AttributeModifierEntryPredicate {
1098 fn matches(&self, modifier: &vanilla_components::ItemAttributeModifierEntry) -> bool {
1099 self.attributes.as_ref().is_none_or(|attributes| {
1100 attributes
1101 .iter()
1102 .any(|attribute| attribute.key == modifier.attribute.key)
1103 }) && self.id.as_ref().is_none_or(|id| id == &modifier.id)
1104 && self.amount.matches(modifier.amount)
1105 && self
1106 .operation
1107 .is_none_or(|operation| operation == modifier.operation)
1108 && self.slot.is_none_or(|slot| slot == modifier.slot)
1109 }
1110}
1111
1112fn parse_attribute_modifier_entry(
1113 compound: &NbtCompound,
1114) -> Option<AttributeModifierEntryPredicate> {
1115 let attributes =
1116 parse_optional_field(compound, "attribute", parse_attribute_holder_set).ok()?;
1117 let id = parse_optional_field(compound, "id", parse_identifier_tag).ok()?;
1118 let amount = parse_optional_field(compound, "amount", parse_double_range)
1119 .ok()?
1120 .unwrap_or(DoubleRange::ANY);
1121 let operation =
1122 parse_optional_field(compound, "operation", parse_attribute_modifier_operation).ok()?;
1123 let slot = parse_optional_field(compound, "slot", parse_equipment_slot_group).ok()?;
1124 Some(AttributeModifierEntryPredicate {
1125 attributes,
1126 id,
1127 amount,
1128 operation,
1129 slot,
1130 })
1131}
1132
1133fn parse_attribute_holder_set(tag: &NbtTag) -> Option<Vec<AttributeRef>> {
1134 match tag {
1135 NbtTag::String(value) => parse_attribute_holder(&value.to_string()),
1136 NbtTag::List(NbtList::Empty) => Some(Vec::new()),
1137 NbtTag::List(NbtList::String(values)) => {
1138 let mut attributes = Vec::new();
1139 for value in values {
1140 attributes.extend(parse_attribute_holder(&value.to_string())?);
1141 }
1142 Some(attributes)
1143 }
1144 _ => None,
1145 }
1146}
1147
1148fn parse_attribute_holder(value: &str) -> Option<Vec<AttributeRef>> {
1149 if value.starts_with('#') {
1150 return None;
1152 }
1153 let key = parse_identifier_with_default_namespace(value)?;
1154 REGISTRY
1155 .attributes
1156 .by_key(&key)
1157 .map(|attribute| vec![attribute])
1158}
1159
1160fn parse_identifier_tag(tag: &NbtTag) -> Option<Identifier> {
1161 let NbtTag::String(value) = tag else {
1162 return None;
1163 };
1164 parse_identifier_with_default_namespace(&value.to_string())
1165}
1166
1167fn parse_attribute_modifier_operation(tag: &NbtTag) -> Option<AttributeModifierOperation> {
1168 let NbtTag::String(value) = tag else {
1169 return None;
1170 };
1171 AttributeModifierOperation::by_name(&value.to_string())
1172}
1173
1174fn parse_equipment_slot_group(tag: &NbtTag) -> Option<EquipmentSlotGroup> {
1175 let NbtTag::String(value) = tag else {
1176 return None;
1177 };
1178 let value = value.to_string();
1179 EquipmentSlotGroup::by_name(&value).filter(|slot| slot.name() == value)
1180}
1181
1182#[derive(Clone, Copy, Debug, PartialEq)]
1183struct DoubleRange {
1184 min: Option<f64>,
1185 max: Option<f64>,
1186}
1187
1188impl DoubleRange {
1189 const ANY: Self = Self {
1190 min: None,
1191 max: None,
1192 };
1193
1194 fn matches(self, value: f64) -> bool {
1195 self.min
1196 .is_none_or(|minimum| minimum.partial_cmp(&value) != Some(Ordering::Greater))
1197 && self
1198 .max
1199 .is_none_or(|maximum| maximum.partial_cmp(&value) != Some(Ordering::Less))
1200 }
1201}
1202
1203fn parse_double_range(tag: &NbtTag) -> Option<DoubleRange> {
1204 if let Some(value) = numeric_f64(tag) {
1205 return Some(DoubleRange {
1206 min: Some(value),
1207 max: Some(value),
1208 });
1209 }
1210 let NbtTag::Compound(compound) = tag else {
1211 return None;
1212 };
1213 let min = parse_optional_field(compound, "min", numeric_f64).ok()?;
1214 let max = parse_optional_field(compound, "max", numeric_f64).ok()?;
1215 if min.zip(max).is_some_and(|(min, max)| min > max) {
1216 return None;
1217 }
1218 Some(DoubleRange { min, max })
1219}
1220
1221fn numeric_f64(tag: &NbtTag) -> Option<f64> {
1222 match tag {
1223 NbtTag::Byte(value) => Some(f64::from(*value)),
1224 NbtTag::Short(value) => Some(f64::from(*value)),
1225 NbtTag::Int(value) => Some(f64::from(*value)),
1226 NbtTag::Long(value) => Some(*value as f64),
1227 NbtTag::Float(value) => Some(f64::from(*value)),
1228 NbtTag::Double(value) => Some(*value),
1229 _ => None,
1230 }
1231}
1232
1233fn parse_optional_field<T>(
1234 compound: &NbtCompound,
1235 key: &str,
1236 parser: impl FnOnce(&NbtTag) -> Option<T>,
1237) -> Result<Option<T>, ()> {
1238 match compound.get(key) {
1239 Some(tag) => parser(tag).map(Some).ok_or(()),
1240 None => Ok(None),
1241 }
1242}
1243
1244fn parse_identifier_with_default_namespace(value: &str) -> Option<Identifier> {
1245 let (namespace, path) = value.split_once(':').map_or(
1246 (Identifier::VANILLA_NAMESPACE, value),
1247 |(namespace, path)| {
1248 if namespace.is_empty() {
1249 (Identifier::VANILLA_NAMESPACE, path)
1250 } else {
1251 (namespace, path)
1252 }
1253 },
1254 );
1255 (!namespace.is_empty() && !path.is_empty() && Identifier::validate(namespace, path))
1256 .then(|| Identifier::new(namespace.to_owned(), path.to_owned()))
1257}
1258
1259pub(super) fn suggest_item_predicate(builder: &mut SuggestionsBuilder<'_>) {
1260 let input = builder.remaining();
1261 let Some(component_start) = input.find('[') else {
1262 suggest_item_targets(input, builder);
1263 if valid_target(input) {
1264 builder.suggest(format!("{input}["));
1265 }
1266 return;
1267 };
1268 if !valid_target(input[..component_start].trim_end()) {
1269 return;
1270 }
1271
1272 let Some((current_start, current)) = current_term(&input[component_start + 1..]) else {
1273 return;
1274 };
1275 if current.contains(['=', '~']) {
1276 return;
1277 }
1278 let trimmed = current.trim_start();
1279 let whitespace = current.len() - trimmed.len();
1280 let prefix_end = component_start + 1 + current_start + whitespace;
1281 let prefix = &input[..prefix_end];
1282 let resource_prefix = trimmed.strip_prefix('!').unwrap_or(trimmed);
1283
1284 for entry in
1285 (0..REGISTRY.data_components.len()).filter_map(|id| REGISTRY.data_components.by_id(id))
1286 {
1287 if entry.is_persistent() && resource_matches(resource_prefix, &entry.key) {
1288 builder.suggest(format!("{prefix}{}", entry.key));
1289 }
1290 }
1291 let count = Identifier::vanilla_static("count");
1292 if resource_matches(resource_prefix, &count) {
1293 builder.suggest(format!("{prefix}{count}"));
1294 }
1295 for path in VANILLA_DATA_COMPONENT_PREDICATE_KEYS {
1296 let key = Identifier::vanilla_static(path);
1297 if resource_matches(resource_prefix, &key) {
1298 builder.suggest(format!("{prefix}{key}"));
1299 }
1300 }
1301}
1302
1303fn suggest_item_targets(input: &str, builder: &mut SuggestionsBuilder<'_>) {
1304 if let Some(prefix) = input.strip_prefix('#') {
1305 for key in REGISTRY.items.tag_keys() {
1306 if resource_matches(prefix, key) {
1307 builder.suggest(format!("#{key}"));
1308 }
1309 }
1310 return;
1311 }
1312 for (_, item) in REGISTRY.items.iter() {
1313 if resource_matches(input, &item.key) {
1314 builder.suggest(item.key.to_string());
1315 }
1316 }
1317 if "*".starts_with(input) {
1318 builder.suggest("*");
1319 }
1320}
1321
1322fn valid_target(input: &str) -> bool {
1323 if input == "*" {
1324 return true;
1325 }
1326 if let Some(tag) = input.strip_prefix('#') {
1327 return parse_identifier_text(tag)
1328 .is_some_and(|tag| REGISTRY.items.get_tag(&tag).is_some());
1329 }
1330 parse_identifier_text(input).is_some_and(|key| REGISTRY.items.by_key(&key).is_some())
1331}
1332
1333fn parse_identifier_text(input: &str) -> Option<Identifier> {
1334 let mut reader = StringReader::new(input);
1335 let key = parse_identifier(&mut reader).ok()?;
1336 (!reader.can_read()).then_some(key)
1337}
1338
1339fn resource_matches(pattern: &str, key: &Identifier) -> bool {
1340 let pattern = pattern.strip_prefix("minecraft:").unwrap_or(pattern);
1341 if pattern.contains(':') {
1342 return matches_substring(pattern, &key.to_string());
1343 }
1344 matches_substring(pattern, key.namespace.as_ref())
1345 || matches_substring(pattern, key.path.as_ref())
1346}
1347
1348fn current_term(conditions: &str) -> Option<(usize, &str)> {
1349 let mut depth = 0usize;
1350 let mut quote = None;
1351 let mut escaped = false;
1352 let mut start = 0usize;
1353
1354 for (index, character) in conditions.char_indices() {
1355 if let Some(terminator) = quote {
1356 if escaped {
1357 escaped = false;
1358 } else if character == '\\' {
1359 escaped = true;
1360 } else if character == terminator {
1361 quote = None;
1362 }
1363 continue;
1364 }
1365 match character {
1366 '"' | '\'' => quote = Some(character),
1367 '{' | '[' => depth += 1,
1368 '}' | ']' if depth > 0 => depth -= 1,
1369 ']' => return None,
1370 ',' | '|' if depth == 0 => start = index + character.len_utf8(),
1371 '!' if depth == 0 && conditions[start..index].trim().is_empty() => {
1372 start = index + character.len_utf8();
1373 }
1374 _ => {}
1375 }
1376 }
1377 Some((start, &conditions[start..]))
1378}