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