Skip to main content

steel_core/command/execution/
argument.rs

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