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