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#[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
85pub(crate) trait SteelArgumentParser:
87 DowncastType + fmt::Debug + PartialEq + Send + Sync + 'static
88{
89 type Value: DowncastType + fmt::Debug + Send + Sync + 'static;
91
92 fn parse(
94 &self,
95 reader: &mut StringReader<'_>,
96 source: &dyn CommandArgumentSource,
97 ) -> Result<Self::Value, CommandSyntaxError>;
98
99 fn list_suggestions(
101 &self,
102 _context: &dyn SteelArgumentSuggestionContext,
103 _builder: &mut SuggestionsBuilder<'_>,
104 ) {
105 }
106
107 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#[derive(Clone)]
160pub(crate) struct SteelArgumentType(Arc<dyn ErasedSteelArgumentParser>);
161
162impl SteelArgumentType {
163 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#[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
436pub(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 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}