Skip to main content

steel_core/command/execution/
argument.rs

1use std::{fmt, sync::Arc};
2
3use super::{
4    BiomeOrTag, BlockPredicate, CommandArgumentSource, Coordinates, IntRange, ItemPredicate,
5    ScoreHolderArgument, StructureOrTagKey, WorldArgument,
6    biome::{parse_biome_or_tag, suggest_biomes},
7    block::{parse_block_predicate, suggest_blocks},
8    coordinates::{parse_block_pos, parse_rotation, parse_vec3, suggest_coordinates},
9    item::{parse_item_stack, suggest_item_stack},
10    item_predicate::{parse_item_predicate, suggest_item_predicate},
11    nbt::parse_nbt_path,
12    permission::{PermissionGroupParser, PermissionMetadataParser, PermissionRuleParser},
13    profile::{GameProfileParser, GameProfileSuggestionMode},
14    score::{parse_int_range, parse_score_holder, suggest_score_holders},
15    selector::{EntitySelector, parse_entity_selector, suggest_entity_selector},
16    structure::{parse_structure_or_tag_key, suggest_structures},
17    text::validate_component_syntax,
18    world::{parse_world_argument, suggest_worlds},
19};
20use crate::chunk::heightmap::HeightmapType;
21use crate::command::brigadier::{
22    ArgumentSuggestionContext, ArgumentType, CommandArgumentParser, CommandSyntaxError,
23    CommandSyntaxErrorKind, ContainsPrimitiveArgumentValue, PrimitiveArgumentValue, StringReader,
24    SuggestionsBuilder,
25};
26use crate::command::protocol::protocol_argument_type;
27use crate::entity::{ENTITIES, EntityAnchor};
28use glam::DVec3;
29use steel_protocol::packets::game::{
30    ArgumentType as ProtocolArgumentType, SuggestionType as ProtocolSuggestionType,
31};
32use steel_registry::damage_type::DamageTypeRef;
33use steel_registry::{
34    DAMAGE_TYPE_REGISTRY, ENCHANTMENT_REGISTRY, ENTITY_TYPE_REGISTRY, REGISTRY, RegistryExt as _,
35    TIMELINE_REGISTRY, WORLD_CLOCK_REGISTRY, enchantment::EnchantmentRef,
36    entity_type::EntityTypeRef, item_stack::ItemStack, timeline::TimelineRef,
37    world_clock::WorldClockRef,
38};
39use steel_utils::{
40    Downcast as _, DowncastType, DowncastTypeKey, ErasedType, Identifier,
41    nbt::{NbtPath, parse_snbt_argument},
42    translations,
43    types::GameType,
44};
45use text_components::TextComponent;
46
47/// Axes selected by vanilla's coordinate swizzle argument.
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub(crate) struct CoordinateAxes(u8);
50
51impl CoordinateAxes {
52    const X: u8 = 1;
53    const Y: u8 = 2;
54    const Z: u8 = 4;
55
56    pub(crate) const fn x(self) -> bool {
57        self.0 & Self::X != 0
58    }
59
60    pub(crate) const fn y(self) -> bool {
61        self.0 & Self::Y != 0
62    }
63
64    pub(crate) const fn z(self) -> bool {
65        self.0 & Self::Z != 0
66    }
67
68    pub(crate) const fn align(self, mut position: DVec3) -> DVec3 {
69        if self.x() {
70            position.x = position.x.floor();
71        }
72        if self.y() {
73            position.y = position.y.floor();
74        }
75        if self.z() {
76            position.z = position.z.floor();
77        }
78        position
79    }
80}
81
82/// Typed parser contract erased by [`SteelArgumentType`].
83pub(crate) trait SteelArgumentParser:
84    DowncastType + fmt::Debug + PartialEq + Send + Sync + 'static
85{
86    /// Concrete value produced by this parser.
87    type Value: DowncastType + fmt::Debug + Send + Sync + 'static;
88
89    /// Parses one value from the command reader.
90    fn parse(
91        &self,
92        reader: &mut StringReader<'_>,
93        source: &dyn CommandArgumentSource,
94    ) -> Result<Self::Value, CommandSyntaxError>;
95
96    /// Adds context-aware completion suggestions.
97    fn list_suggestions(
98        &self,
99        _context: &dyn SteelArgumentSuggestionContext,
100        _builder: &mut SuggestionsBuilder<'_>,
101    ) {
102    }
103
104    /// Returns the vanilla command-tree parser representation.
105    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>);
106}
107
108trait ErasedSteelArgumentParser: ErasedType + fmt::Debug + Send + Sync {
109    fn parse_erased(
110        &self,
111        reader: &mut StringReader<'_>,
112        source: &dyn CommandArgumentSource,
113    ) -> Result<SteelArgumentValue, CommandSyntaxError>;
114
115    fn list_suggestions_erased(
116        &self,
117        context: &dyn SteelArgumentSuggestionContext,
118        builder: &mut SuggestionsBuilder<'_>,
119    );
120
121    fn protocol_argument_erased(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>);
122
123    fn equals_erased(&self, other: &dyn ErasedSteelArgumentParser) -> bool;
124}
125
126impl<P> ErasedSteelArgumentParser for P
127where
128    P: SteelArgumentParser,
129{
130    fn parse_erased(
131        &self,
132        reader: &mut StringReader<'_>,
133        source: &dyn CommandArgumentSource,
134    ) -> Result<SteelArgumentValue, CommandSyntaxError> {
135        self.parse(reader, source).map(SteelArgumentValue::new)
136    }
137
138    fn list_suggestions_erased(
139        &self,
140        context: &dyn SteelArgumentSuggestionContext,
141        builder: &mut SuggestionsBuilder<'_>,
142    ) {
143        self.list_suggestions(context, builder);
144    }
145
146    fn protocol_argument_erased(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
147        self.protocol_argument()
148    }
149
150    fn equals_erased(&self, other: &dyn ErasedSteelArgumentParser) -> bool {
151        other.downcast_ref::<P>() == Some(self)
152    }
153}
154
155/// An extensible, keyed parser stored by Steel's command runtime.
156#[derive(Clone)]
157pub(crate) struct SteelArgumentType(Arc<dyn ErasedSteelArgumentParser>);
158
159impl SteelArgumentType {
160    /// Erases a concrete parser while retaining its deterministic type key.
161    pub(crate) fn new(parser: impl SteelArgumentParser) -> Self {
162        Self(Arc::new(parser))
163    }
164
165    pub(crate) fn time(minimum: i32) -> Self {
166        Self::new(TimeParser { minimum })
167    }
168
169    pub(crate) fn block_pos() -> Self {
170        Self::new(BlockPosParser)
171    }
172
173    pub(crate) fn vec3(center_integers: bool) -> Self {
174        Self::new(Vec3Parser { center_integers })
175    }
176
177    pub(crate) fn rotation() -> Self {
178        Self::new(RotationParser)
179    }
180
181    pub(crate) fn swizzle() -> Self {
182        Self::new(SwizzleParser)
183    }
184
185    pub(crate) fn heightmap() -> Self {
186        Self::new(HeightmapParser)
187    }
188
189    pub(crate) fn entity_anchor() -> Self {
190        Self::new(EntityAnchorParser)
191    }
192
193    pub(crate) fn entity() -> Self {
194        Self::new(EntityParser {
195            single: true,
196            players_only: false,
197        })
198    }
199
200    pub(crate) fn entities() -> Self {
201        Self::new(EntityParser {
202            single: false,
203            players_only: false,
204        })
205    }
206
207    pub(crate) fn player() -> Self {
208        Self::new(EntityParser {
209            single: true,
210            players_only: true,
211        })
212    }
213
214    pub(crate) fn players() -> Self {
215        Self::new(EntityParser {
216            single: false,
217            players_only: true,
218        })
219    }
220
221    pub(crate) fn score_holder() -> Self {
222        Self::new(ScoreHolderParser { multiple: false })
223    }
224
225    pub(crate) fn non_operator_profile() -> Self {
226        Self::new(GameProfileParser::new(
227            GameProfileSuggestionMode::NonOperators,
228        ))
229    }
230
231    pub(crate) fn game_profile() -> Self {
232        Self::new(GameProfileParser::new(GameProfileSuggestionMode::All))
233    }
234
235    pub(crate) fn operator_profile() -> Self {
236        Self::new(GameProfileParser::new(GameProfileSuggestionMode::Operators))
237    }
238
239    pub(crate) fn permission_rule() -> Self {
240        Self::new(PermissionRuleParser::all())
241    }
242
243    pub(crate) fn user_permission_rule() -> Self {
244        Self::new(PermissionRuleParser::user_owned())
245    }
246
247    pub(crate) fn group_permission_rule() -> Self {
248        Self::new(PermissionRuleParser::group_owned())
249    }
250
251    pub(crate) fn permission_metadata() -> Self {
252        Self::new(PermissionMetadataParser::all())
253    }
254
255    pub(crate) fn user_permission_metadata() -> Self {
256        Self::new(PermissionMetadataParser::user_owned())
257    }
258
259    pub(crate) fn group_permission_metadata() -> Self {
260        Self::new(PermissionMetadataParser::group_owned())
261    }
262
263    pub(crate) fn permission_group(require_existing: bool) -> Self {
264        Self::new(PermissionGroupParser { require_existing })
265    }
266
267    pub(crate) fn score_holders() -> Self {
268        Self::new(ScoreHolderParser { multiple: true })
269    }
270
271    pub(crate) fn objective() -> Self {
272        Self::new(ObjectiveParser)
273    }
274
275    pub(crate) fn int_range() -> Self {
276        Self::new(IntRangeParser)
277    }
278
279    pub(crate) fn biome_or_tag() -> Self {
280        Self::new(BiomeOrTagParser)
281    }
282
283    pub(crate) fn structure_or_tag_key() -> Self {
284        Self::new(StructureOrTagKeyParser)
285    }
286
287    pub(crate) fn block_predicate() -> Self {
288        Self::new(BlockPredicateParser)
289    }
290    pub(crate) fn block_state() -> Self {
291        Self::new(BlockStateParser)
292    }
293
294    pub(crate) fn game_mode() -> Self {
295        Self::new(GameModeParser)
296    }
297
298    pub(crate) fn domain() -> Self {
299        Self::new(DomainParser)
300    }
301
302    pub(crate) fn world() -> Self {
303        Self::new(WorldParser)
304    }
305
306    pub(crate) fn summonable_entity() -> Self {
307        Self::new(SummonableEntityParser)
308    }
309
310    pub(crate) fn enchantment() -> Self {
311        Self::new(EnchantmentParser)
312    }
313
314    pub(crate) fn damage_type() -> Self {
315        Self::new(DamageTypeParser)
316    }
317
318    pub(crate) fn item_stack() -> Self {
319        Self::new(ItemStackParser)
320    }
321
322    pub(crate) fn item_predicate() -> Self {
323        Self::new(ItemPredicateParser)
324    }
325
326    pub(crate) fn component() -> Self {
327        Self::new(ComponentParser)
328    }
329
330    pub(crate) fn nbt_path() -> Self {
331        Self::new(NbtPathParser)
332    }
333
334    pub(crate) fn storage_key() -> Self {
335        Self::new(StorageKeyParser)
336    }
337
338    pub(crate) fn world_clock() -> Self {
339        Self::new(WorldClockParser)
340    }
341
342    pub(crate) fn timeline(clock_argument: Option<&'static str>) -> Self {
343        Self::new(TimelineParser { clock_argument })
344    }
345
346    pub(crate) fn time_marker(clock_argument: Option<&'static str>) -> Self {
347        Self::new(TimeMarkerParser { clock_argument })
348    }
349
350    pub(crate) fn protocol_argument(
351        &self,
352    ) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
353        self.0.protocol_argument_erased()
354    }
355
356    #[cfg(test)]
357    pub(crate) fn parser_type_key(&self) -> DowncastTypeKey {
358        self.0.downcast_type_key()
359    }
360}
361
362impl fmt::Debug for SteelArgumentType {
363    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
364        formatter
365            .debug_struct("SteelArgumentType")
366            .field("type_key", &self.0.downcast_type_key())
367            .field("parser", &self.0)
368            .finish()
369    }
370}
371
372impl PartialEq for SteelArgumentType {
373    fn eq(&self, other: &Self) -> bool {
374        self.0.equals_erased(other.0.as_ref())
375    }
376}
377
378impl From<ArgumentType> for SteelArgumentType {
379    fn from(argument: ArgumentType) -> Self {
380        Self::new(PrimitiveParser(argument))
381    }
382}
383
384trait ErasedSteelArgumentValue: ErasedType + fmt::Debug + Send + Sync {}
385
386impl<T> ErasedSteelArgumentValue for T where T: DowncastType + fmt::Debug + Send + Sync {}
387
388/// A keyed parsed value retained by Steel's command runtime.
389#[derive(Clone)]
390pub(crate) struct SteelArgumentValue(Arc<dyn ErasedSteelArgumentValue>);
391
392impl SteelArgumentValue {
393    pub(crate) fn new(value: impl DowncastType + fmt::Debug + Send + Sync) -> Self {
394        Self(Arc::new(value))
395    }
396
397    pub(crate) fn downcast_ref<T: DowncastType>(&self) -> Option<&T> {
398        self.0.downcast_ref::<T>()
399    }
400
401    #[cfg(test)]
402    pub(crate) fn type_key(&self) -> DowncastTypeKey {
403        self.0.downcast_type_key()
404    }
405}
406
407impl fmt::Debug for SteelArgumentValue {
408    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
409        formatter
410            .debug_struct("SteelArgumentValue")
411            .field("type_key", &self.0.downcast_type_key())
412            .field("value", &self.0)
413            .finish()
414    }
415}
416
417impl ContainsPrimitiveArgumentValue for SteelArgumentValue {
418    fn primitive_value(&self) -> Option<&PrimitiveArgumentValue> {
419        self.downcast_ref::<PrimitiveArgumentValue>()
420    }
421}
422
423/// Suggestion context exposed to erased Steel argument parsers.
424pub(crate) trait SteelArgumentSuggestionContext {
425    fn source(&self) -> &dyn CommandArgumentSource;
426
427    fn argument(&self, name: &str) -> Option<&SteelArgumentValue>;
428}
429
430impl<S> SteelArgumentSuggestionContext for ArgumentSuggestionContext<'_, S, SteelArgumentValue>
431where
432    S: CommandArgumentSource,
433{
434    fn source(&self) -> &dyn CommandArgumentSource {
435        ArgumentSuggestionContext::source(self)
436    }
437
438    fn argument(&self, name: &str) -> Option<&SteelArgumentValue> {
439        ArgumentSuggestionContext::argument(self, name)
440    }
441}
442
443impl<S> CommandArgumentParser<S> for SteelArgumentType
444where
445    S: CommandArgumentSource,
446{
447    type Value = SteelArgumentValue;
448
449    fn parse(
450        &self,
451        reader: &mut StringReader<'_>,
452        source: &S,
453    ) -> Result<Self::Value, CommandSyntaxError> {
454        self.0.parse_erased(reader, source)
455    }
456
457    fn list_suggestions(
458        &self,
459        context: &ArgumentSuggestionContext<'_, S, Self::Value>,
460        builder: &mut SuggestionsBuilder<'_>,
461    ) {
462        self.0.list_suggestions_erased(context, builder);
463    }
464}
465
466macro_rules! impl_downcast_type {
467    ($type:ty, $key:literal) => {
468        // SAFETY: This Steel-owned key uniquely identifies the concrete type in the process.
469        unsafe impl DowncastType for $type {
470            const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new($key);
471        }
472    };
473}
474
475impl_downcast_type!(PrimitiveArgumentValue, "steel:command/value/primitive");
476impl_downcast_type!(Coordinates, "steel:command/value/coordinates");
477impl_downcast_type!(EntityAnchor, "steel:command/value/entity_anchor");
478impl_downcast_type!(CoordinateAxes, "steel:command/value/swizzle");
479impl_downcast_type!(HeightmapType, "steel:command/value/heightmap");
480impl_downcast_type!(EntitySelector, "steel:command/value/entity_selector");
481impl_downcast_type!(ScoreHolderArgument, "steel:command/value/score_holder");
482impl_downcast_type!(IntRange, "steel:command/value/int_range");
483impl_downcast_type!(BiomeOrTag, "steel:command/value/biome_or_tag");
484impl_downcast_type!(
485    StructureOrTagKey,
486    "steel:command/value/structure_or_tag_key"
487);
488impl_downcast_type!(BlockPredicate, "steel:command/value/block_predicate");
489impl_downcast_type!(WorldArgument, "steel:command/value/world");
490impl_downcast_type!(ItemPredicate, "steel:command/value/item_predicate");
491
492macro_rules! argument_value_wrapper {
493    ($name:ident($value:ty), $key:literal) => {
494        #[derive(Debug)]
495        pub(super) struct $name(pub(super) $value);
496
497        impl_downcast_type!($name, $key);
498    };
499}
500
501argument_value_wrapper!(TimeValue(i32), "steel:command/value/time");
502argument_value_wrapper!(ObjectiveValue(Box<str>), "steel:command/value/objective");
503argument_value_wrapper!(GameModeValue(GameType), "steel:command/value/game_mode");
504argument_value_wrapper!(DomainValue(Box<str>), "steel:command/value/domain");
505argument_value_wrapper!(
506    EntityTypeValue(EntityTypeRef),
507    "steel:command/value/entity_type"
508);
509argument_value_wrapper!(
510    EnchantmentValue(EnchantmentRef),
511    "steel:command/value/enchantment"
512);
513argument_value_wrapper!(
514    DamageTypeValue(DamageTypeRef),
515    "steel:command/value/damage_type"
516);
517argument_value_wrapper!(ItemStackValue(ItemStack), "steel:command/value/item_stack");
518argument_value_wrapper!(
519    ComponentValue(TextComponent),
520    "steel:command/value/component"
521);
522argument_value_wrapper!(NbtPathValue(NbtPath), "steel:command/value/nbt_path");
523argument_value_wrapper!(
524    IdentifierValue(Identifier),
525    "steel:command/value/identifier"
526);
527argument_value_wrapper!(
528    WorldClockValue(WorldClockRef),
529    "steel:command/value/world_clock"
530);
531argument_value_wrapper!(TimelineValue(TimelineRef), "steel:command/value/timeline");
532
533macro_rules! unit_argument_parser {
534    (
535        $parser:ident,
536        $key:literal,
537        $value:ty,
538        parse |$reader:ident, $source:ident| $parse:block,
539        suggest |$context:ident, $builder:ident| $suggest:block,
540        protocol $protocol:expr
541    ) => {
542        #[derive(Clone, Copy, Debug, PartialEq, Eq)]
543        struct $parser;
544
545        impl_downcast_type!($parser, $key);
546
547        impl SteelArgumentParser for $parser {
548            type Value = $value;
549
550            fn parse(
551                &self,
552                $reader: &mut StringReader<'_>,
553                $source: &dyn CommandArgumentSource,
554            ) -> Result<Self::Value, CommandSyntaxError> $parse
555
556            fn list_suggestions(
557                &self,
558                $context: &dyn SteelArgumentSuggestionContext,
559                $builder: &mut SuggestionsBuilder<'_>,
560            ) $suggest
561
562            fn protocol_argument(
563                &self,
564            ) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
565                $protocol
566            }
567        }
568    };
569}
570
571#[derive(Clone, Debug, PartialEq)]
572struct PrimitiveParser(ArgumentType);
573
574impl_downcast_type!(PrimitiveParser, "steel:command/parser/primitive");
575
576impl SteelArgumentParser for PrimitiveParser {
577    type Value = PrimitiveArgumentValue;
578
579    fn parse(
580        &self,
581        reader: &mut StringReader<'_>,
582        _source: &dyn CommandArgumentSource,
583    ) -> Result<Self::Value, CommandSyntaxError> {
584        self.0.parse_value(reader)
585    }
586
587    fn list_suggestions(
588        &self,
589        _context: &dyn SteelArgumentSuggestionContext,
590        builder: &mut SuggestionsBuilder<'_>,
591    ) {
592        self.0.suggest(builder);
593    }
594
595    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
596        (protocol_argument_type(&self.0), None)
597    }
598}
599
600#[derive(Clone, Copy, Debug, PartialEq, Eq)]
601struct TimeParser {
602    minimum: i32,
603}
604
605impl_downcast_type!(TimeParser, "steel:command/parser/time");
606
607impl SteelArgumentParser for TimeParser {
608    type Value = TimeValue;
609
610    fn parse(
611        &self,
612        reader: &mut StringReader<'_>,
613        _source: &dyn CommandArgumentSource,
614    ) -> Result<Self::Value, CommandSyntaxError> {
615        parse_time(reader, self.minimum).map(TimeValue)
616    }
617
618    fn list_suggestions(
619        &self,
620        _context: &dyn SteelArgumentSuggestionContext,
621        builder: &mut SuggestionsBuilder<'_>,
622    ) {
623        suggest_time_units(builder);
624    }
625
626    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
627        (ProtocolArgumentType::Time { min: self.minimum }, None)
628    }
629}
630
631unit_argument_parser!(
632    BlockPosParser,
633    "steel:command/parser/block_pos",
634    Coordinates,
635    parse | reader,
636    _source | { parse_block_pos(reader) },
637    suggest | _context,
638    builder | {
639        suggest_coordinates(builder, parse_block_pos);
640    },
641    protocol(ProtocolArgumentType::BlockPos, None)
642);
643
644#[derive(Clone, Copy, Debug, PartialEq, Eq)]
645struct Vec3Parser {
646    center_integers: bool,
647}
648
649impl_downcast_type!(Vec3Parser, "steel:command/parser/vec3");
650
651impl SteelArgumentParser for Vec3Parser {
652    type Value = Coordinates;
653
654    fn parse(
655        &self,
656        reader: &mut StringReader<'_>,
657        _source: &dyn CommandArgumentSource,
658    ) -> Result<Self::Value, CommandSyntaxError> {
659        parse_vec3(reader, self.center_integers)
660    }
661
662    fn list_suggestions(
663        &self,
664        _context: &dyn SteelArgumentSuggestionContext,
665        builder: &mut SuggestionsBuilder<'_>,
666    ) {
667        suggest_coordinates(builder, |reader| parse_vec3(reader, self.center_integers));
668    }
669
670    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
671        (ProtocolArgumentType::Vec3, None)
672    }
673}
674
675unit_argument_parser!(
676    RotationParser,
677    "steel:command/parser/rotation",
678    Coordinates,
679    parse | reader,
680    _source | { parse_rotation(reader) },
681    suggest | _context,
682    _builder | {},
683    protocol(ProtocolArgumentType::Rotation, None)
684);
685unit_argument_parser!(
686    SwizzleParser,
687    "steel:command/parser/swizzle",
688    CoordinateAxes,
689    parse | reader,
690    _source | { parse_swizzle(reader) },
691    suggest | _context,
692    _builder | {},
693    protocol(ProtocolArgumentType::Swizzle, None)
694);
695unit_argument_parser!(
696    HeightmapParser,
697    "steel:command/parser/heightmap",
698    HeightmapType,
699    parse | reader,
700    _source | { parse_heightmap(reader) },
701    suggest | _context,
702    builder | {
703        suggest_heightmaps(builder);
704    },
705    protocol(ProtocolArgumentType::Heightmap, None)
706);
707unit_argument_parser!(
708    EntityAnchorParser,
709    "steel:command/parser/entity_anchor",
710    EntityAnchor,
711    parse | reader,
712    _source | { parse_entity_anchor(reader) },
713    suggest | _context,
714    builder | {
715        suggest_entity_anchors(builder);
716    },
717    protocol(ProtocolArgumentType::EntityAnchor, None)
718);
719
720#[derive(Clone, Copy, Debug, PartialEq, Eq)]
721struct EntityParser {
722    single: bool,
723    players_only: bool,
724}
725
726impl_downcast_type!(EntityParser, "steel:command/parser/entity");
727
728impl SteelArgumentParser for EntityParser {
729    type Value = EntitySelector;
730
731    fn parse(
732        &self,
733        reader: &mut StringReader<'_>,
734        source: &dyn CommandArgumentSource,
735    ) -> Result<Self::Value, CommandSyntaxError> {
736        parse_entity_selector(reader, source, self.single, self.players_only)
737    }
738
739    fn list_suggestions(
740        &self,
741        context: &dyn SteelArgumentSuggestionContext,
742        builder: &mut SuggestionsBuilder<'_>,
743    ) {
744        suggest_entity_selector(builder, context.source(), self.single, self.players_only);
745    }
746
747    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
748        (
749            ProtocolArgumentType::Entity {
750                flags: u8::from(self.single) | (u8::from(self.players_only) << 1),
751            },
752            None,
753        )
754    }
755}
756
757#[derive(Clone, Copy, Debug, PartialEq, Eq)]
758struct ScoreHolderParser {
759    multiple: bool,
760}
761
762impl_downcast_type!(ScoreHolderParser, "steel:command/parser/score_holder");
763
764impl SteelArgumentParser for ScoreHolderParser {
765    type Value = ScoreHolderArgument;
766
767    fn parse(
768        &self,
769        reader: &mut StringReader<'_>,
770        source: &dyn CommandArgumentSource,
771    ) -> Result<Self::Value, CommandSyntaxError> {
772        parse_score_holder(reader, source, self.multiple)
773    }
774
775    fn list_suggestions(
776        &self,
777        context: &dyn SteelArgumentSuggestionContext,
778        builder: &mut SuggestionsBuilder<'_>,
779    ) {
780        suggest_score_holders(builder, context.source());
781    }
782
783    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
784        (
785            ProtocolArgumentType::ScoreHolder {
786                flags: u8::from(self.multiple),
787            },
788            Some(ProtocolSuggestionType::AskServer),
789        )
790    }
791}
792
793unit_argument_parser!(
794    ObjectiveParser,
795    "steel:command/parser/objective",
796    ObjectiveValue,
797    parse | reader,
798    _source | { Ok(ObjectiveValue(reader.read_unquoted_string().into())) },
799    suggest | context,
800    builder | {
801        let prefix = builder.remaining();
802        for objective in context
803            .source()
804            .scoreboard_objective_names()
805            .into_iter()
806            .filter(|objective| objective.starts_with(prefix))
807        {
808            builder.suggest(objective);
809        }
810    },
811    protocol(
812        ProtocolArgumentType::Objective,
813        Some(ProtocolSuggestionType::AskServer),
814    )
815);
816unit_argument_parser!(
817    IntRangeParser,
818    "steel:command/parser/int_range",
819    IntRange,
820    parse | reader,
821    _source | { parse_int_range(reader) },
822    suggest | _context,
823    _builder | {},
824    protocol(ProtocolArgumentType::IntRange, None)
825);
826unit_argument_parser!(
827    BiomeOrTagParser,
828    "steel:command/parser/biome_or_tag",
829    BiomeOrTag,
830    parse | reader,
831    _source | { parse_biome_or_tag(reader) },
832    suggest | _context,
833    builder | {
834        suggest_biomes(builder);
835    },
836    protocol(
837        ProtocolArgumentType::ResourceOrTag {
838            identifier: "minecraft:worldgen/biome",
839        },
840        Some(ProtocolSuggestionType::AskServer),
841    )
842);
843unit_argument_parser!(
844    StructureOrTagKeyParser,
845    "steel:command/parser/structure_or_tag_key",
846    StructureOrTagKey,
847    parse | reader,
848    _source | { parse_structure_or_tag_key(reader) },
849    suggest | _context,
850    builder | {
851        suggest_structures(builder);
852    },
853    protocol(
854        ProtocolArgumentType::ResourceOrTagKey {
855            identifier: "minecraft:worldgen/structure",
856        },
857        Some(ProtocolSuggestionType::AskServer),
858    )
859);
860unit_argument_parser!(
861    BlockPredicateParser,
862    "steel:command/parser/block_predicate",
863    BlockPredicate,
864    parse | reader,
865    _source | { parse_block_predicate(reader) },
866    suggest | _context,
867    builder | {
868        suggest_blocks(builder);
869    },
870    protocol(ProtocolArgumentType::BlockPredicate, None)
871);
872unit_argument_parser!(
873    BlockStateParser,
874    "steel:command/parser/block_state",
875    BlockPredicate,
876    parse | reader,
877    _source | { parse_block_predicate(reader) },
878    suggest | _context,
879    _builder | {},
880    protocol(ProtocolArgumentType::BlockState, None)
881);
882unit_argument_parser!(
883    GameModeParser,
884    "steel:command/parser/game_mode",
885    GameModeValue,
886    parse | reader,
887    _source | { parse_game_mode(reader).map(GameModeValue) },
888    suggest | _context,
889    builder | {
890        suggest_game_modes(builder);
891    },
892    protocol(ProtocolArgumentType::Gamemode, None)
893);
894unit_argument_parser!(
895    DomainParser,
896    "steel:command/parser/domain",
897    DomainValue,
898    parse | reader,
899    source | { parse_domain(reader, source).map(DomainValue) },
900    suggest | context,
901    builder | {
902        let prefix = builder.remaining();
903        for domain in context
904            .source()
905            .domain_names()
906            .into_iter()
907            .filter(|domain| domain.starts_with(prefix))
908        {
909            builder.suggest(domain);
910        }
911    },
912    protocol(
913        ProtocolArgumentType::ResourceLocation,
914        Some(ProtocolSuggestionType::AskServer),
915    )
916);
917unit_argument_parser!(
918    WorldParser,
919    "steel:command/parser/world",
920    WorldArgument,
921    parse | reader,
922    _source | { parse_world_argument(reader) },
923    suggest | context,
924    builder | {
925        suggest_worlds(builder, context.source());
926    },
927    protocol(
928        ProtocolArgumentType::Dimension,
929        Some(ProtocolSuggestionType::AskServer),
930    )
931);
932unit_argument_parser!(
933    SummonableEntityParser,
934    "steel:command/parser/summonable_entity",
935    EntityTypeValue,
936    parse | reader,
937    _source | { parse_summonable_entity(reader).map(EntityTypeValue) },
938    suggest | _context,
939    builder | {
940        suggest_resources(
941            REGISTRY
942                .entity_types
943                .iter()
944                .filter(|(_, entity_type)| can_summon(entity_type))
945                .map(|(_, entity_type)| &entity_type.key),
946            builder,
947        );
948    },
949    protocol(
950        ProtocolArgumentType::Resource {
951            identifier: "minecraft:entity_type",
952        },
953        Some(ProtocolSuggestionType::SummonableEntities),
954    )
955);
956unit_argument_parser!(
957    EnchantmentParser,
958    "steel:command/parser/enchantment",
959    EnchantmentValue,
960    parse | reader,
961    _source | {
962        let key = parse_identifier(reader)?;
963        REGISTRY.enchantments.by_key(&key).map_or_else(
964            || Err(unknown_resource(reader, &key, &ENCHANTMENT_REGISTRY)),
965            |enchantment| Ok(EnchantmentValue(enchantment)),
966        )
967    },
968    suggest | _context,
969    builder | {
970        suggest_resources(
971            REGISTRY
972                .enchantments
973                .iter()
974                .map(|(_, enchantment)| &enchantment.key),
975            builder,
976        );
977    },
978    protocol(
979        ProtocolArgumentType::Resource {
980            identifier: "minecraft:enchantment",
981        },
982        None,
983    )
984);
985unit_argument_parser!(
986    DamageTypeParser,
987    "steel:command/parser/damage_type",
988    DamageTypeValue,
989    parse | reader,
990    _source | {
991        let key = parse_identifier(reader)?;
992        REGISTRY.damage_types.by_key(&key).map_or_else(
993            || Err(unknown_resource(reader, &key, &DAMAGE_TYPE_REGISTRY)),
994            |damage_type| Ok(DamageTypeValue(damage_type)),
995        )
996    },
997    suggest | _context,
998    builder | {
999        suggest_resources(
1000            REGISTRY
1001                .damage_types
1002                .iter()
1003                .map(|(_, damage_type)| &damage_type.key),
1004            builder,
1005        );
1006    },
1007    protocol(
1008        ProtocolArgumentType::Resource {
1009            identifier: "minecraft:damage_type",
1010        },
1011        None,
1012    )
1013);
1014unit_argument_parser!(
1015    ItemStackParser,
1016    "steel:command/parser/item_stack",
1017    ItemStackValue,
1018    parse | reader,
1019    _source | { parse_item_stack(reader).map(ItemStackValue) },
1020    suggest | _context,
1021    builder | {
1022        suggest_item_stack(builder);
1023    },
1024    protocol(
1025        ProtocolArgumentType::ItemStack,
1026        Some(ProtocolSuggestionType::AskServer),
1027    )
1028);
1029unit_argument_parser!(
1030    ItemPredicateParser,
1031    "steel:command/parser/item_predicate",
1032    ItemPredicate,
1033    parse | reader,
1034    _source | { parse_item_predicate(reader) },
1035    suggest | _context,
1036    builder | {
1037        suggest_item_predicate(builder);
1038    },
1039    protocol(
1040        ProtocolArgumentType::ItemPredicate,
1041        Some(ProtocolSuggestionType::AskServer),
1042    )
1043);
1044unit_argument_parser!(
1045    ComponentParser,
1046    "steel:command/parser/component",
1047    ComponentValue,
1048    parse | reader,
1049    _source | { parse_component(reader).map(ComponentValue) },
1050    suggest | _context,
1051    _builder | {},
1052    protocol(ProtocolArgumentType::Component, None)
1053);
1054unit_argument_parser!(
1055    NbtPathParser,
1056    "steel:command/parser/nbt_path",
1057    NbtPathValue,
1058    parse | reader,
1059    _source | { parse_nbt_path(reader).map(NbtPathValue) },
1060    suggest | _context,
1061    _builder | {},
1062    protocol(ProtocolArgumentType::NbtPath, None)
1063);
1064unit_argument_parser!(
1065    StorageKeyParser,
1066    "steel:command/parser/storage_key",
1067    IdentifierValue,
1068    parse | reader,
1069    _source | { parse_identifier(reader).map(IdentifierValue) },
1070    suggest | context,
1071    builder | {
1072        suggest_storage_keys(context.source(), builder);
1073    },
1074    protocol(
1075        ProtocolArgumentType::ResourceLocation,
1076        Some(ProtocolSuggestionType::AskServer),
1077    )
1078);
1079unit_argument_parser!(
1080    WorldClockParser,
1081    "steel:command/parser/world_clock",
1082    WorldClockValue,
1083    parse | reader,
1084    _source | {
1085        let key = parse_identifier(reader)?;
1086        REGISTRY.world_clocks.by_key(&key).map_or_else(
1087            || Err(unknown_resource(reader, &key, &WORLD_CLOCK_REGISTRY)),
1088            |clock| Ok(WorldClockValue(clock)),
1089        )
1090    },
1091    suggest | _context,
1092    builder | {
1093        suggest_resources(
1094            REGISTRY.world_clocks.iter().map(|(_, clock)| &clock.key),
1095            builder,
1096        );
1097    },
1098    protocol(
1099        ProtocolArgumentType::Resource {
1100            identifier: "minecraft:world_clock",
1101        },
1102        None,
1103    )
1104);
1105
1106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1107struct TimelineParser {
1108    clock_argument: Option<&'static str>,
1109}
1110
1111impl_downcast_type!(TimelineParser, "steel:command/parser/timeline");
1112
1113impl SteelArgumentParser for TimelineParser {
1114    type Value = TimelineValue;
1115
1116    fn parse(
1117        &self,
1118        reader: &mut StringReader<'_>,
1119        _source: &dyn CommandArgumentSource,
1120    ) -> Result<Self::Value, CommandSyntaxError> {
1121        let key = parse_identifier(reader)?;
1122        REGISTRY.timelines.by_key(&key).map_or_else(
1123            || Err(unknown_resource(reader, &key, &TIMELINE_REGISTRY)),
1124            |timeline| Ok(TimelineValue(timeline)),
1125        )
1126    }
1127
1128    fn list_suggestions(
1129        &self,
1130        context: &dyn SteelArgumentSuggestionContext,
1131        builder: &mut SuggestionsBuilder<'_>,
1132    ) {
1133        let Some(clock) = selected_clock(context, self.clock_argument) else {
1134            return;
1135        };
1136        suggest_resources(
1137            REGISTRY
1138                .timelines
1139                .iter()
1140                .filter(|(_, timeline)| timeline.clock == clock)
1141                .map(|(_, timeline)| &timeline.key),
1142            builder,
1143        );
1144    }
1145
1146    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
1147        (
1148            ProtocolArgumentType::Resource {
1149                identifier: "minecraft:timeline",
1150            },
1151            Some(ProtocolSuggestionType::AskServer),
1152        )
1153    }
1154}
1155
1156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1157struct TimeMarkerParser {
1158    clock_argument: Option<&'static str>,
1159}
1160
1161impl_downcast_type!(TimeMarkerParser, "steel:command/parser/time_marker");
1162
1163impl SteelArgumentParser for TimeMarkerParser {
1164    type Value = IdentifierValue;
1165
1166    fn parse(
1167        &self,
1168        reader: &mut StringReader<'_>,
1169        _source: &dyn CommandArgumentSource,
1170    ) -> Result<Self::Value, CommandSyntaxError> {
1171        parse_identifier(reader).map(IdentifierValue)
1172    }
1173
1174    fn list_suggestions(
1175        &self,
1176        context: &dyn SteelArgumentSuggestionContext,
1177        builder: &mut SuggestionsBuilder<'_>,
1178    ) {
1179        let Some(clock) = selected_clock(context, self.clock_argument) else {
1180            return;
1181        };
1182        suggest_resources(
1183            REGISTRY
1184                .timelines
1185                .iter()
1186                .filter(|(_, timeline)| timeline.clock == clock)
1187                .flat_map(|(_, timeline)| timeline.time_markers)
1188                .filter(|marker| marker.show_in_commands == Some(true))
1189                .map(|marker| &marker.key),
1190            builder,
1191        );
1192    }
1193
1194    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<ProtocolSuggestionType>) {
1195        (
1196            ProtocolArgumentType::ResourceLocation,
1197            Some(ProtocolSuggestionType::AskServer),
1198        )
1199    }
1200}
1201
1202fn selected_clock(
1203    context: &dyn SteelArgumentSuggestionContext,
1204    clock_argument: Option<&str>,
1205) -> Option<WorldClockRef> {
1206    let Some(clock_argument) = clock_argument else {
1207        return context.source().default_world_clock();
1208    };
1209    context
1210        .argument(clock_argument)?
1211        .downcast_ref::<WorldClockValue>()
1212        .map(|clock| clock.0)
1213}
1214
1215fn parse_component(reader: &mut StringReader<'_>) -> Result<TextComponent, CommandSyntaxError> {
1216    let start = reader.checkpoint();
1217    let (tag, consumed) = parse_snbt_argument(reader.remaining()).map_err(|error| {
1218        reader.advance_bytes(error.cursor());
1219        component_snbt_error(reader, error.component())
1220    })?;
1221    if !reader.advance_bytes(consumed) {
1222        return Err(component_snbt_error(
1223            reader,
1224            "Invalid text component cursor",
1225        ));
1226    }
1227
1228    let component = TextComponent::try_from_nbt(&tag).map_err(|error| {
1229        reader.restore(start);
1230        invalid_component(reader, error.to_string())
1231    })?;
1232    validate_component_syntax(&component).map_err(|error| {
1233        reader.restore(start);
1234        invalid_component(reader, error)
1235    })?;
1236    Ok(component)
1237}
1238
1239fn component_snbt_error(
1240    reader: &StringReader<'_>,
1241    message: impl Into<TextComponent>,
1242) -> CommandSyntaxError {
1243    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message.into())))
1244}
1245
1246fn invalid_component(reader: &StringReader<'_>, message: String) -> CommandSyntaxError {
1247    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
1248        translations::ARGUMENT_COMPONENT_INVALID
1249            .message([message])
1250            .component(),
1251    )))
1252}
1253
1254fn parse_swizzle(reader: &mut StringReader<'_>) -> Result<CoordinateAxes, CommandSyntaxError> {
1255    let mut axes = CoordinateAxes::default();
1256    while reader.can_read() && reader.peek() != Some(' ') {
1257        let bit = match reader.read() {
1258            Some('x') => CoordinateAxes::X,
1259            Some('y') => CoordinateAxes::Y,
1260            Some('z') => CoordinateAxes::Z,
1261            Some(_) | None => return Err(invalid_swizzle(reader)),
1262        };
1263        if axes.0 & bit != 0 {
1264            return Err(invalid_swizzle(reader));
1265        }
1266        axes.0 |= bit;
1267    }
1268    Ok(axes)
1269}
1270
1271fn invalid_swizzle(reader: &StringReader<'_>) -> CommandSyntaxError {
1272    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
1273        TextComponent::from(&translations::ARGUMENTS_SWIZZLE_INVALID),
1274    )))
1275}
1276
1277fn parse_heightmap(reader: &mut StringReader<'_>) -> Result<HeightmapType, CommandSyntaxError> {
1278    let raw = reader.read_unquoted_string();
1279    match raw.to_ascii_lowercase().as_str() {
1280        "world_surface" => Ok(HeightmapType::WorldSurface),
1281        "motion_blocking" => Ok(HeightmapType::MotionBlocking),
1282        "motion_blocking_no_leaves" => Ok(HeightmapType::MotionBlockingNoLeaves),
1283        "ocean_floor" => Ok(HeightmapType::OceanFloor),
1284        _ => {
1285            let message = translations::ARGUMENT_ENUM_INVALID
1286                .message([raw.to_owned()])
1287                .component();
1288            Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))))
1289        }
1290    }
1291}
1292
1293fn suggest_heightmaps(builder: &mut SuggestionsBuilder<'_>) {
1294    const HEIGHTMAPS: &[&str] = &[
1295        "world_surface",
1296        "motion_blocking",
1297        "motion_blocking_no_leaves",
1298        "ocean_floor",
1299    ];
1300    for heightmap in HEIGHTMAPS {
1301        if heightmap.starts_with(builder.remaining_lowercase()) {
1302            builder.suggest(*heightmap);
1303        }
1304    }
1305}
1306
1307fn parse_entity_anchor(reader: &mut StringReader<'_>) -> Result<EntityAnchor, CommandSyntaxError> {
1308    let start = reader.checkpoint();
1309    let name = reader.read_unquoted_string();
1310    match name {
1311        "feet" => Ok(EntityAnchor::Feet),
1312        "eyes" => Ok(EntityAnchor::Eyes),
1313        _ => {
1314            reader.restore(start);
1315            let message = translations::ARGUMENT_ANCHOR_INVALID
1316                .message([name.to_owned()])
1317                .component();
1318            Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))))
1319        }
1320    }
1321}
1322
1323fn suggest_entity_anchors(builder: &mut SuggestionsBuilder<'_>) {
1324    let prefix = builder.remaining_lowercase().to_owned();
1325    for anchor in ["feet", "eyes"] {
1326        if anchor.starts_with(&prefix) {
1327            builder.suggest(anchor);
1328        }
1329    }
1330}
1331
1332fn parse_game_mode(reader: &mut StringReader<'_>) -> Result<GameType, CommandSyntaxError> {
1333    let name = reader.read_unquoted_string();
1334    let game_mode = match name {
1335        "survival" => GameType::Survival,
1336        "creative" => GameType::Creative,
1337        "adventure" => GameType::Adventure,
1338        "spectator" => GameType::Spectator,
1339        _ => {
1340            let message = translations::ARGUMENT_GAMEMODE_INVALID
1341                .message([name.to_owned()])
1342                .component();
1343            return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))));
1344        }
1345    };
1346    Ok(game_mode)
1347}
1348
1349fn suggest_game_modes(builder: &mut SuggestionsBuilder<'_>) {
1350    let prefix = builder.remaining_lowercase().to_owned();
1351    for game_mode in [
1352        GameType::Survival,
1353        GameType::Creative,
1354        GameType::Adventure,
1355        GameType::Spectator,
1356    ] {
1357        let name = game_mode.name();
1358        if name.starts_with(&prefix) {
1359            builder.suggest(name);
1360        }
1361    }
1362}
1363
1364fn parse_summonable_entity(
1365    reader: &mut StringReader<'_>,
1366) -> Result<EntityTypeRef, CommandSyntaxError> {
1367    let key = parse_identifier(reader)?;
1368    let Some(entity_type) = REGISTRY.entity_types.by_key(&key) else {
1369        return Err(unknown_resource(reader, &key, &ENTITY_TYPE_REGISTRY));
1370    };
1371    if can_summon(entity_type) {
1372        return Ok(entity_type);
1373    }
1374    let message = translations::ENTITY_NOT_SUMMONABLE
1375        .message([key.to_string()])
1376        .component();
1377    Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))))
1378}
1379
1380fn can_summon(entity_type: EntityTypeRef) -> bool {
1381    entity_type.summonable
1382        && ENTITIES
1383            .get()
1384            .is_some_and(|registry| registry.has_factory(entity_type))
1385}
1386
1387fn parse_domain<S>(
1388    reader: &mut StringReader<'_>,
1389    source: &S,
1390) -> Result<Box<str>, CommandSyntaxError>
1391where
1392    S: CommandArgumentSource + ?Sized,
1393{
1394    let domain = reader.read_unquoted_string();
1395    if source.domain_exists(domain) {
1396        return Ok(domain.into());
1397    }
1398    Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
1399        TextComponent::from(format!("Unknown domain {domain}")),
1400    ))))
1401}
1402
1403pub(super) fn parse_identifier(
1404    reader: &mut StringReader<'_>,
1405) -> Result<Identifier, CommandSyntaxError> {
1406    let start = reader.checkpoint();
1407    let start_byte = reader.read_so_far().len();
1408    while reader.peek().is_some_and(is_allowed_in_identifier) {
1409        reader.skip();
1410    }
1411    let raw = &reader.read_so_far()[start_byte..];
1412    let (namespace, path) =
1413        raw.split_once(':')
1414            .map_or((Identifier::VANILLA_NAMESPACE, raw), |(namespace, path)| {
1415                if namespace.is_empty() {
1416                    (Identifier::VANILLA_NAMESPACE, path)
1417                } else {
1418                    (namespace, path)
1419                }
1420            });
1421    if namespace != ".."
1422        && Identifier::validate_namespace(namespace)
1423        && Identifier::validate_path(path)
1424    {
1425        return Ok(Identifier::new(namespace.to_owned(), path.to_owned()));
1426    }
1427
1428    reader.restore(start);
1429    Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
1430        TextComponent::from(&translations::ARGUMENT_ID_INVALID),
1431    ))))
1432}
1433
1434const fn is_allowed_in_identifier(character: char) -> bool {
1435    character.is_ascii_digit()
1436        || character.is_ascii_lowercase()
1437        || matches!(character, '_' | ':' | '/' | '.' | '-')
1438}
1439
1440pub(super) fn unknown_resource(
1441    reader: &StringReader<'_>,
1442    key: &Identifier,
1443    registry: &Identifier,
1444) -> CommandSyntaxError {
1445    let message = translations::ARGUMENT_RESOURCE_NOT_FOUND
1446        .message([key.to_string(), registry.to_string()])
1447        .component();
1448    reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message)))
1449}
1450
1451fn suggest_resources<'a>(
1452    resources: impl Iterator<Item = &'a Identifier>,
1453    builder: &mut SuggestionsBuilder<'_>,
1454) {
1455    let contents = builder.remaining_lowercase();
1456    let has_namespace = contents.contains(':');
1457    let suggestions = resources.filter_map(|resource| {
1458        let full_name = resource.to_string();
1459        let matches = if has_namespace {
1460            matches_substring(contents, &full_name)
1461        } else {
1462            matches_substring(contents, resource.namespace.as_ref())
1463                || matches_substring(contents, resource.path.as_ref())
1464        };
1465        matches.then_some(full_name)
1466    });
1467    let suggestions = suggestions.collect::<Vec<_>>();
1468    for suggestion in suggestions {
1469        builder.suggest(suggestion);
1470    }
1471}
1472
1473fn suggest_storage_keys<S>(source: &S, builder: &mut SuggestionsBuilder<'_>)
1474where
1475    S: CommandArgumentSource + ?Sized,
1476{
1477    let keys = source
1478        .command_storage_keys()
1479        .into_iter()
1480        .filter_map(|key| key.parse::<Identifier>().ok())
1481        .collect::<Vec<_>>();
1482    suggest_resources(keys.iter(), builder);
1483}
1484
1485pub(super) fn matches_substring(pattern: &str, input: &str) -> bool {
1486    if input.starts_with(pattern) {
1487        return true;
1488    }
1489    input.char_indices().any(|(index, character)| {
1490        matches!(character, '.' | '_' | '/')
1491            && input[index + character.len_utf8()..].starts_with(pattern)
1492    })
1493}
1494
1495pub(super) fn identifier_matches(pattern: &str, identifier: &Identifier) -> bool {
1496    if pattern.contains(':') {
1497        matches_substring(pattern, &identifier.to_string())
1498    } else {
1499        matches_substring(pattern, identifier.namespace.as_ref())
1500            || matches_substring(pattern, identifier.path.as_ref())
1501    }
1502}
1503
1504fn parse_time(reader: &mut StringReader<'_>, minimum: i32) -> Result<i32, CommandSyntaxError> {
1505    let value = reader.read_float()?;
1506    let unit = reader.read_unquoted_string();
1507    let factor = match unit {
1508        "d" => 24_000.0,
1509        "s" => 20.0,
1510        "t" | "" => 1.0,
1511        _ => {
1512            return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(
1513                TextComponent::from(&translations::ARGUMENT_TIME_INVALID_UNIT),
1514            ))));
1515        }
1516    };
1517    let ticks = java_round(value * factor);
1518    if ticks < minimum {
1519        let message = translations::ARGUMENT_TIME_TICK_COUNT_TOO_LOW
1520            .message([minimum.to_string(), ticks.to_string()])
1521            .component();
1522        return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))));
1523    }
1524    Ok(ticks)
1525}
1526
1527fn suggest_time_units(builder: &mut SuggestionsBuilder<'_>) {
1528    let mut reader = StringReader::new(builder.remaining());
1529    if reader.read_float().is_err() {
1530        return;
1531    }
1532    let number = reader.read_so_far();
1533    let unit = reader.read_unquoted_string();
1534    for candidate in ["d", "s", "t"] {
1535        if candidate.starts_with(unit) {
1536            builder.suggest(format!("{number}{candidate}"));
1537        }
1538    }
1539}
1540
1541fn java_round(value: f32) -> i32 {
1542    (value + 0.5).floor() as i32
1543}