Skip to main content

steel_core/command/
protocol.rs

1use rustc_hash::FxHashMap;
2use steel_protocol::packets::game::{
3    ArgumentStringTypeBehavior, ArgumentType as ProtocolArgumentType, CCommandSuggestions,
4    CCommands, CommandNode as ProtocolCommandNode, CommandNodeInfo, SuggestionEntry,
5    SuggestionType,
6};
7use thiserror::Error;
8
9use super::brigadier::{
10    ArgumentType, CommandDispatcher, CommandRuntime, NodeId, NodeKind, StringType, Suggestions,
11};
12use super::execution::SteelArgumentType;
13
14const MAX_COMMAND_SUGGESTIONS: usize = 1000;
15
16/// Converts Brigadier suggestions to the vanilla command-suggestion packet.
17pub(crate) fn command_suggestions_packet(
18    transaction_id: i32,
19    suggestions: &Suggestions,
20) -> CCommandSuggestions {
21    let range = suggestions.range();
22    // Serverbound command suggestions are bounded to 32,500 bytes, so their
23    // UTF-16 indices always fit the packet's signed VarInts.
24    let start = range.start() as i32;
25    let length = range.len() as i32;
26    let entries = suggestions
27        .list()
28        .iter()
29        .take(MAX_COMMAND_SUGGESTIONS)
30        .map(|suggestion| match suggestion.tooltip() {
31            Some(tooltip) => SuggestionEntry::with_tooltip(suggestion.text(), tooltip.clone()),
32            None => SuggestionEntry::new(suggestion.text()),
33        })
34        .collect();
35    CCommandSuggestions::new(transaction_id, start, length, entries)
36}
37
38/// A filtered Brigadier graph could not be represented by the vanilla packet.
39#[derive(Clone, Debug, Error, PartialEq, Eq)]
40pub(crate) enum CommandTreeProjectionError {
41    #[error("command graph contains an unknown node {0:?}")]
42    UnknownNode(NodeId),
43    #[error("command graph contains more nodes than the protocol can index")]
44    TooManyNodes,
45    #[error("argument node {0:?} has no argument parser")]
46    MissingArgumentType(NodeId),
47    #[error("visible command node {node:?} redirects to filtered node {target:?}")]
48    HiddenRedirectTarget { node: NodeId, target: NodeId },
49}
50
51/// Builds the command graph visible to one source using vanilla packet indices.
52pub(crate) fn command_tree_packet<S, R>(
53    dispatcher: &CommandDispatcher<S, R>,
54    source: &S,
55) -> Result<CCommands, CommandTreeProjectionError>
56where
57    R: CommandRuntime<S>,
58    R::Argument: CommandArgumentProtocol,
59{
60    let visible = visible_nodes(dispatcher, source)?;
61    let mut indices = FxHashMap::default();
62    for (index, node) in visible.iter().copied().enumerate() {
63        let Ok(index) = i32::try_from(index) else {
64            return Err(CommandTreeProjectionError::TooManyNodes);
65        };
66        indices.insert(node, index);
67    }
68
69    let mut nodes = Vec::with_capacity(visible.len());
70    for node_id in visible {
71        let node = dispatcher
72            .node(node_id)
73            .ok_or(CommandTreeProjectionError::UnknownNode(node_id))?;
74        let children = dispatcher
75            .children(node_id)
76            .ok_or(CommandTreeProjectionError::UnknownNode(node_id))?
77            .iter()
78            .filter_map(|child| indices.get(child).copied())
79            .collect();
80        let redirect = match node.redirect() {
81            Some(target) => Some(indices.get(&target).copied().ok_or(
82                CommandTreeProjectionError::HiddenRedirectTarget {
83                    node: node_id,
84                    target,
85                },
86            )?),
87            None => None,
88        };
89
90        let mut info = CommandNodeInfo::new(children);
91        if node.can_execute(source) {
92            info = info.executable();
93        }
94        if node.is_restricted() {
95            info = info.restricted();
96        }
97        if let Some(target) = redirect {
98            info = info.redirect(target);
99        }
100
101        let projected = match node.kind() {
102            NodeKind::Root => {
103                let mut root = ProtocolCommandNode::new_root();
104                root.set_children(
105                    dispatcher
106                        .children(node_id)
107                        .ok_or(CommandTreeProjectionError::UnknownNode(node_id))?
108                        .iter()
109                        .filter_map(|child| indices.get(child).copied())
110                        .collect(),
111                );
112                root
113            }
114            NodeKind::Literal => ProtocolCommandNode::new_literal(info, node.name().to_owned()),
115            NodeKind::Argument => {
116                let argument = node
117                    .argument_type()
118                    .ok_or(CommandTreeProjectionError::MissingArgumentType(node_id))?;
119                ProtocolCommandNode::new_argument(
120                    info,
121                    node.name().to_owned(),
122                    argument.protocol_argument(),
123                )
124            }
125        };
126        nodes.push(projected);
127    }
128
129    Ok(CCommands {
130        nodes,
131        root_index: 0,
132    })
133}
134
135fn visible_nodes<S, R>(
136    dispatcher: &CommandDispatcher<S, R>,
137    source: &S,
138) -> Result<Vec<NodeId>, CommandTreeProjectionError>
139where
140    R: CommandRuntime<S>,
141{
142    let mut visible = Vec::new();
143    let mut pending = vec![dispatcher.root()];
144    while let Some(node_id) = pending.pop() {
145        visible.push(node_id);
146        let children = dispatcher
147            .children(node_id)
148            .ok_or(CommandTreeProjectionError::UnknownNode(node_id))?;
149        for child in children.iter().rev() {
150            let node = dispatcher
151                .node(*child)
152                .ok_or(CommandTreeProjectionError::UnknownNode(*child))?;
153            if node.allows(source) {
154                pending.push(*child);
155            }
156        }
157    }
158    Ok(visible)
159}
160
161pub(crate) trait CommandArgumentProtocol {
162    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<SuggestionType>);
163}
164
165impl CommandArgumentProtocol for ArgumentType {
166    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<SuggestionType>) {
167        (protocol_argument_type(self), None)
168    }
169}
170
171impl CommandArgumentProtocol for SteelArgumentType {
172    fn protocol_argument(&self) -> (ProtocolArgumentType, Option<SuggestionType>) {
173        SteelArgumentType::protocol_argument(self)
174    }
175}
176
177pub(super) fn protocol_argument_type(argument: &ArgumentType) -> ProtocolArgumentType {
178    match *argument {
179        ArgumentType::Bool => ProtocolArgumentType::Bool,
180        ArgumentType::Integer { minimum, maximum } => ProtocolArgumentType::Integer {
181            min: (minimum != i32::MIN).then_some(minimum),
182            max: (maximum != i32::MAX).then_some(maximum),
183        },
184        ArgumentType::Long { minimum, maximum } => ProtocolArgumentType::Long {
185            min: (minimum != i64::MIN).then_some(minimum),
186            max: (maximum != i64::MAX).then_some(maximum),
187        },
188        ArgumentType::Float { minimum, maximum } => ProtocolArgumentType::Float {
189            min: (minimum.to_bits() != (-f32::MAX).to_bits()).then_some(minimum),
190            max: (maximum.to_bits() != f32::MAX.to_bits()).then_some(maximum),
191        },
192        ArgumentType::Double { minimum, maximum } => ProtocolArgumentType::Double {
193            min: (minimum.to_bits() != (-f64::MAX).to_bits()).then_some(minimum),
194            max: (maximum.to_bits() != f64::MAX.to_bits()).then_some(maximum),
195        },
196        ArgumentType::String(string_type) => ProtocolArgumentType::String {
197            behavior: match string_type {
198                StringType::Word => ArgumentStringTypeBehavior::SingleWord,
199                StringType::QuotablePhrase => ArgumentStringTypeBehavior::QuotablePhrase,
200                StringType::GreedyPhrase => ArgumentStringTypeBehavior::GreedyPhrase,
201            },
202        },
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::{
209        CommandTreeProjectionError, MAX_COMMAND_SUGGESTIONS, command_suggestions_packet,
210        command_tree_packet, protocol_argument_type,
211    };
212    use crate::command::brigadier::{
213        ArgumentType, CommandDispatcher, CommandNodeBuilder, CommandRequirement,
214        CommandSyntaxError, NodeId, StringRange, StringReader, StringType, Suggestion, Suggestions,
215        argument, literal,
216    };
217    use crate::command::execution::{
218        CommandArgumentSource, CommandResultCallback, ExecutionCommandSource, SteelArgumentType,
219        SteelCommandRuntime, argument as steel_argument, literal as steel_literal,
220    };
221    use steel_protocol::packets::game::{
222        ArgumentStringTypeBehavior, ArgumentType as ProtocolArgumentType,
223        CommandNode as ProtocolCommandNode, SuggestionType,
224    };
225    use steel_utils::serial::WriteTo;
226    use text_components::TextComponent;
227
228    #[derive(Clone, Copy)]
229    struct TestSource {
230        authorized: bool,
231        in_context: bool,
232    }
233
234    #[derive(Clone, Copy)]
235    struct SteelTestSource;
236
237    impl CommandArgumentSource for SteelTestSource {}
238
239    impl ExecutionCommandSource for SteelTestSource {
240        fn with_callback(&self, _callback: CommandResultCallback) -> Self {
241            *self
242        }
243
244        fn callback(&self) -> CommandResultCallback {
245            CommandResultCallback::empty()
246        }
247
248        fn handle_error(&self, _error: &CommandSyntaxError, _forked: bool) {}
249    }
250
251    fn register(
252        dispatcher: &mut CommandDispatcher<TestSource>,
253        builder: CommandNodeBuilder<TestSource>,
254    ) -> NodeId {
255        let Ok(node) = dispatcher.register(builder) else {
256            panic!("test command should register");
257        };
258        node
259    }
260
261    #[test]
262    fn leading_slash_remains_in_the_packet_replacement_range() {
263        let mut dispatcher = CommandDispatcher::<()>::new();
264        assert!(dispatcher.register(literal("help")).is_ok());
265        let mut reader = StringReader::new("/he");
266        assert!(reader.skip());
267
268        let parse = dispatcher.parse_reader(reader, ());
269        let suggestions = dispatcher.completion_suggestions(&parse);
270        let Ok(suggestions) = suggestions else {
271            panic!("slash-prefixed suggestions should build");
272        };
273        let packet = command_suggestions_packet(7, &suggestions);
274
275        assert_eq!(packet.id, 7);
276        assert_eq!(packet.start, 1);
277        assert_eq!(packet.length, 2);
278        assert_eq!(packet.suggestions.len(), 1);
279        assert_eq!(packet.suggestions[0].text, "help");
280    }
281
282    #[test]
283    fn packet_projection_preserves_utf16_range_and_tooltip() {
284        let tooltip = TextComponent::plain("details");
285        let suggestions = Suggestions::new(
286            StringRange::between(2, 4),
287            vec![Suggestion::with_tooltip(
288                StringRange::between(2, 4),
289                "value",
290                tooltip.clone(),
291            )],
292        );
293
294        let packet = command_suggestions_packet(11, &suggestions);
295
296        assert_eq!(packet.start, 2);
297        assert_eq!(packet.length, 2);
298        assert_eq!(packet.suggestions[0].text, "value");
299        assert_eq!(packet.suggestions[0].tooltip.as_ref(), Some(&tooltip));
300    }
301
302    #[test]
303    fn packet_projection_applies_vanillas_suggestion_limit() {
304        let range = StringRange::at(0);
305        let suggestions = Suggestions::new(
306            range,
307            (0..=MAX_COMMAND_SUGGESTIONS)
308                .map(|index| Suggestion::new(range, index.to_string()))
309                .collect(),
310        );
311
312        let packet = command_suggestions_packet(1, &suggestions);
313
314        assert_eq!(packet.suggestions.len(), MAX_COMMAND_SUGGESTIONS);
315        assert_eq!(packet.suggestions[0].text, "0");
316        assert_eq!(packet.suggestions[MAX_COMMAND_SUGGESTIONS - 1].text, "999");
317    }
318
319    #[test]
320    fn command_tree_filters_both_requirement_kinds_but_marks_only_authorization() {
321        let mut dispatcher = CommandDispatcher::new();
322        register(&mut dispatcher, literal("public").executes(|_| Ok(1)));
323        register(
324            &mut dispatcher,
325            literal("admin")
326                .requires(CommandRequirement::authorization(|source: &TestSource| {
327                    source.authorized
328                }))
329                .executes(|_| Ok(1)),
330        );
331        register(
332            &mut dispatcher,
333            literal("nearby")
334                .requires(CommandRequirement::contextual(|source: &TestSource| {
335                    source.in_context
336                }))
337                .executes(|_| Ok(1)),
338        );
339
340        let denied = command_tree_packet(
341            &dispatcher,
342            &TestSource {
343                authorized: false,
344                in_context: false,
345            },
346        );
347        let Ok(denied) = denied else {
348            panic!("denied command tree should project");
349        };
350        assert_eq!(denied.nodes.len(), 2);
351        let ProtocolCommandNode::Root { children } = &denied.nodes[0] else {
352            panic!("first projected node should be the root");
353        };
354        assert_eq!(children, &[1]);
355
356        let allowed = command_tree_packet(
357            &dispatcher,
358            &TestSource {
359                authorized: true,
360                in_context: true,
361            },
362        );
363        let Ok(allowed) = allowed else {
364            panic!("allowed command tree should project");
365        };
366        assert_eq!(allowed.nodes.len(), 4);
367        let ProtocolCommandNode::Literal {
368            name,
369            is_restricted,
370            ..
371        } = &allowed.nodes[2]
372        else {
373            panic!("authorization command should be a literal");
374        };
375        assert_eq!(name, "admin");
376        assert!(*is_restricted);
377        let ProtocolCommandNode::Literal {
378            name,
379            is_restricted,
380            ..
381        } = &allowed.nodes[3]
382        else {
383            panic!("context command should be a literal");
384        };
385        assert_eq!(name, "nearby");
386        assert!(!is_restricted);
387    }
388
389    #[test]
390    fn command_tree_hides_a_denied_executor_without_hiding_its_route() {
391        let mut dispatcher = CommandDispatcher::new();
392        register(
393            &mut dispatcher,
394            literal("route")
395                .executes(|_| Ok(1))
396                .also_requires_execution(CommandRequirement::authorization(
397                    |source: &TestSource| source.authorized,
398                ))
399                .then(literal("child").executes(|_| Ok(2))),
400        );
401
402        let denied = command_tree_packet(
403            &dispatcher,
404            &TestSource {
405                authorized: false,
406                in_context: true,
407            },
408        );
409        let Ok(denied) = denied else {
410            panic!("denied command tree should project");
411        };
412        let ProtocolCommandNode::Literal {
413            name,
414            is_executable,
415            is_restricted,
416            ..
417        } = &denied.nodes[1]
418        else {
419            panic!("route should remain visible");
420        };
421        assert_eq!(name, "route");
422        assert!(!is_executable);
423        assert!(*is_restricted);
424
425        let allowed = command_tree_packet(
426            &dispatcher,
427            &TestSource {
428                authorized: true,
429                in_context: true,
430            },
431        );
432        let Ok(allowed) = allowed else {
433            panic!("allowed command tree should project");
434        };
435        let ProtocolCommandNode::Literal { is_executable, .. } = &allowed.nodes[1] else {
436            panic!("route should remain visible");
437        };
438        assert!(*is_executable);
439    }
440
441    #[test]
442    fn command_tree_remaps_redirects_after_filtering() {
443        let mut dispatcher = CommandDispatcher::new();
444        let target = register(&mut dispatcher, literal("target").executes(|_| Ok(1)));
445        register(
446            &mut dispatcher,
447            literal("hidden").requires(CommandRequirement::contextual(|source: &TestSource| {
448                source.in_context
449            })),
450        );
451        register(&mut dispatcher, literal("alias").redirects(target));
452
453        let packet = command_tree_packet(
454            &dispatcher,
455            &TestSource {
456                authorized: false,
457                in_context: false,
458            },
459        );
460        let Ok(packet) = packet else {
461            panic!("visible redirect target should project");
462        };
463        assert_eq!(packet.nodes.len(), 3);
464        let ProtocolCommandNode::Literal {
465            name, redirects_to, ..
466        } = &packet.nodes[2]
467        else {
468            panic!("alias should be a literal");
469        };
470        assert_eq!(name, "alias");
471        assert_eq!(*redirects_to, Some(1));
472    }
473
474    #[test]
475    fn command_tree_rejects_visible_redirects_to_filtered_targets() {
476        let mut dispatcher = CommandDispatcher::new();
477        let target = register(
478            &mut dispatcher,
479            literal("target").requires(CommandRequirement::contextual(|source: &TestSource| {
480                source.in_context
481            })),
482        );
483        let alias = register(&mut dispatcher, literal("alias").redirects(target));
484
485        let result = command_tree_packet(
486            &dispatcher,
487            &TestSource {
488                authorized: false,
489                in_context: false,
490            },
491        );
492
493        assert!(matches!(
494            result,
495            Err(CommandTreeProjectionError::HiddenRedirectTarget {
496                node,
497                target: hidden_target,
498            }) if node == alias && hidden_target == target
499        ));
500    }
501
502    #[test]
503    fn command_tree_projects_executable_primitive_arguments() {
504        let mut dispatcher = CommandDispatcher::new();
505        register(
506            &mut dispatcher,
507            literal("number")
508                .then(argument("value", ArgumentType::integer(1, i32::MAX)).executes(|_| Ok(1))),
509        );
510
511        let packet = command_tree_packet(
512            &dispatcher,
513            &TestSource {
514                authorized: false,
515                in_context: false,
516            },
517        );
518        let Ok(packet) = packet else {
519            panic!("primitive argument tree should project");
520        };
521        let ProtocolCommandNode::Argument {
522            name,
523            is_executable,
524            parser: ProtocolArgumentType::Integer { min, max },
525            ..
526        } = &packet.nodes[2]
527        else {
528            panic!("third node should be an integer argument");
529        };
530        assert_eq!(name, "value");
531        assert!(*is_executable);
532        assert_eq!(*min, Some(1));
533        assert_eq!(*max, None);
534    }
535
536    #[test]
537    fn permission_expression_nodes_project_as_terminal_greedy_strings() {
538        let mut dispatcher = CommandDispatcher::<SteelTestSource, SteelCommandRuntime>::new();
539        let command = steel_literal::<SteelTestSource>("perms")
540            .then(steel_argument::<SteelTestSource>(
541                "permission",
542                SteelArgumentType::permission_rule(),
543            ))
544            .then(steel_argument::<SteelTestSource>(
545                "metadata",
546                SteelArgumentType::permission_metadata(),
547            ))
548            .then(steel_argument::<SteelTestSource>(
549                "group",
550                SteelArgumentType::permission_group(true),
551            ));
552        assert!(dispatcher.register(command).is_ok());
553
554        let packet = command_tree_packet(&dispatcher, &SteelTestSource);
555        let Ok(packet) = packet else {
556            panic!("permission command tree should project");
557        };
558
559        for name in ["permission", "metadata"] {
560            let Some(node) = packet.nodes.iter().find(|node| {
561                matches!(
562                    node,
563                    ProtocolCommandNode::Argument { name: node_name, .. }
564                        if node_name == name
565                )
566            }) else {
567                panic!("permission command tree should contain {name}");
568            };
569            let ProtocolCommandNode::Argument {
570                parser,
571                suggestions_type,
572                ..
573            } = node
574            else {
575                unreachable!();
576            };
577            assert!(matches!(
578                parser,
579                ProtocolArgumentType::String {
580                    behavior: ArgumentStringTypeBehavior::GreedyPhrase
581                }
582            ));
583            assert!(matches!(suggestions_type, Some(SuggestionType::AskServer)));
584        }
585
586        let Some(group) = packet.nodes.iter().find(|node| {
587            matches!(
588                node,
589                ProtocolCommandNode::Argument { name, .. } if name == "group"
590            )
591        }) else {
592            panic!("permission command tree should contain group");
593        };
594        assert!(matches!(
595            group,
596            ProtocolCommandNode::Argument {
597                parser: ProtocolArgumentType::String {
598                    behavior: ArgumentStringTypeBehavior::SingleWord
599                },
600                suggestions_type: Some(SuggestionType::AskServer),
601                ..
602            }
603        ));
604    }
605
606    #[test]
607    fn primitive_argument_projection_omits_default_bounds_and_maps_strings() {
608        assert!(matches!(
609            protocol_argument_type(&ArgumentType::float(-f32::MAX, f32::MAX)),
610            ProtocolArgumentType::Float {
611                min: None,
612                max: None
613            }
614        ));
615        assert!(matches!(
616            protocol_argument_type(&ArgumentType::String(StringType::GreedyPhrase)),
617            ProtocolArgumentType::String {
618                behavior: ArgumentStringTypeBehavior::GreedyPhrase
619            }
620        ));
621    }
622
623    #[test]
624    fn steel_time_argument_projects_its_minimum() {
625        let (argument, suggestions) = SteelArgumentType::time(1).protocol_argument();
626        let ProtocolArgumentType::Time { min } = argument else {
627            panic!("Steel time parser should project as the protocol time argument");
628        };
629
630        assert_eq!(min, 1);
631        assert!(suggestions.is_none());
632    }
633
634    #[test]
635    fn steel_position_arguments_project_vanilla_parsers() {
636        let (block_pos, block_pos_suggestions) = SteelArgumentType::block_pos().protocol_argument();
637        assert!(matches!(block_pos, ProtocolArgumentType::BlockPos));
638        assert!(block_pos_suggestions.is_none());
639
640        let (vec3, vec3_suggestions) = SteelArgumentType::vec3(true).protocol_argument();
641        assert!(matches!(vec3, ProtocolArgumentType::Vec3));
642        assert!(vec3_suggestions.is_none());
643
644        let (rotation, rotation_suggestions) = SteelArgumentType::rotation().protocol_argument();
645        assert!(matches!(rotation, ProtocolArgumentType::Rotation));
646        assert!(rotation_suggestions.is_none());
647
648        let (anchor, anchor_suggestions) = SteelArgumentType::entity_anchor().protocol_argument();
649        assert!(matches!(anchor, ProtocolArgumentType::EntityAnchor));
650        assert!(anchor_suggestions.is_none());
651
652        let (swizzle, swizzle_suggestions) = SteelArgumentType::swizzle().protocol_argument();
653        assert!(matches!(swizzle, ProtocolArgumentType::Swizzle));
654        assert!(swizzle_suggestions.is_none());
655
656        let (heightmap, heightmap_suggestions) = SteelArgumentType::heightmap().protocol_argument();
657        let mut encoded_heightmap = Vec::new();
658        assert!(matches!(heightmap, ProtocolArgumentType::Heightmap));
659        assert!(heightmap.write(&mut encoded_heightmap).is_ok());
660        assert_eq!(encoded_heightmap, [51]);
661        assert!(heightmap_suggestions.is_none());
662    }
663
664    #[test]
665    fn steel_entity_arguments_use_vanilla_client_suggestions() {
666        for (argument, expected_flags) in [
667            (SteelArgumentType::entities(), 0),
668            (SteelArgumentType::entity(), 1),
669            (SteelArgumentType::players(), 2),
670            (SteelArgumentType::player(), 3),
671        ] {
672            let (argument, suggestions) = argument.protocol_argument();
673            assert!(matches!(
674                argument,
675                ProtocolArgumentType::Entity { flags } if flags == expected_flags
676            ));
677            assert!(suggestions.is_none());
678        }
679    }
680
681    #[test]
682    fn steel_scoreboard_arguments_project_vanilla_parsers() {
683        for (argument, expected_flags) in [
684            (SteelArgumentType::score_holder(), 0),
685            (SteelArgumentType::score_holders(), 1),
686        ] {
687            let (argument, suggestions) = argument.protocol_argument();
688            assert!(matches!(
689                argument,
690                ProtocolArgumentType::ScoreHolder { flags } if flags == expected_flags
691            ));
692            assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
693        }
694
695        let (objective, objective_suggestions) = SteelArgumentType::objective().protocol_argument();
696        assert!(matches!(objective, ProtocolArgumentType::Objective));
697        assert!(matches!(
698            objective_suggestions,
699            Some(SuggestionType::AskServer)
700        ));
701
702        let (range, range_suggestions) = SteelArgumentType::int_range().protocol_argument();
703        assert!(matches!(range, ProtocolArgumentType::IntRange));
704        assert!(range_suggestions.is_none());
705    }
706
707    #[test]
708    fn steel_biome_argument_projects_vanilla_resource_or_tag_parser() {
709        let (argument, suggestions) = SteelArgumentType::biome_or_tag().protocol_argument();
710
711        assert!(matches!(
712            argument,
713            ProtocolArgumentType::ResourceOrTag {
714                identifier: "minecraft:worldgen/biome"
715            }
716        ));
717        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
718    }
719
720    #[test]
721    fn steel_structure_argument_projects_vanilla_resource_or_tag_key_parser() {
722        let (argument, suggestions) = SteelArgumentType::structure_or_tag_key().protocol_argument();
723
724        assert!(matches!(
725            argument,
726            ProtocolArgumentType::ResourceOrTagKey {
727                identifier: "minecraft:worldgen/structure"
728            }
729        ));
730        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
731    }
732
733    #[test]
734    fn steel_block_predicate_argument_projects_vanillas_parser() {
735        let (argument, suggestions) = SteelArgumentType::block_predicate().protocol_argument();
736
737        assert!(matches!(argument, ProtocolArgumentType::BlockPredicate));
738        assert!(suggestions.is_none());
739    }
740
741    #[test]
742    fn steel_nbt_path_argument_projects_vanillas_parser() {
743        let (argument, suggestions) = SteelArgumentType::nbt_path().protocol_argument();
744
745        assert!(matches!(argument, ProtocolArgumentType::NbtPath));
746        assert!(suggestions.is_none());
747    }
748
749    #[test]
750    fn steel_component_argument_projects_vanillas_parser() {
751        let (argument, suggestions) = SteelArgumentType::component().protocol_argument();
752
753        assert!(matches!(argument, ProtocolArgumentType::Component));
754        assert!(suggestions.is_none());
755    }
756
757    #[test]
758    fn steel_storage_key_argument_projects_vanillas_identifier_parser() {
759        let (argument, suggestions) = SteelArgumentType::storage_key().protocol_argument();
760
761        assert!(matches!(argument, ProtocolArgumentType::ResourceLocation));
762        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
763    }
764
765    #[test]
766    fn steel_world_argument_projects_vanillas_dimension_parser() {
767        let (argument, suggestions) = SteelArgumentType::world().protocol_argument();
768
769        assert!(matches!(argument, ProtocolArgumentType::Dimension));
770        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
771    }
772
773    #[test]
774    fn steel_game_mode_argument_projects_vanillas_parser() {
775        let (argument, suggestions) = SteelArgumentType::game_mode().protocol_argument();
776
777        assert!(matches!(argument, ProtocolArgumentType::Gamemode));
778        assert!(suggestions.is_none());
779    }
780
781    #[test]
782    fn steel_enchantment_argument_projects_vanillas_registry_parser() {
783        let (argument, suggestions) = SteelArgumentType::enchantment().protocol_argument();
784
785        assert!(matches!(
786            argument,
787            ProtocolArgumentType::Resource {
788                identifier: "minecraft:enchantment"
789            }
790        ));
791        assert!(suggestions.is_none());
792    }
793
794    #[test]
795    fn steel_item_stack_argument_asks_the_server_for_vanilla_parser_suggestions() {
796        let (argument, suggestions) = SteelArgumentType::item_stack().protocol_argument();
797
798        assert!(matches!(argument, ProtocolArgumentType::ItemStack));
799        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
800    }
801
802    #[test]
803    fn steel_item_predicate_argument_asks_the_server_for_vanilla_parser_suggestions() {
804        let (argument, suggestions) = SteelArgumentType::item_predicate().protocol_argument();
805
806        assert!(matches!(argument, ProtocolArgumentType::ItemPredicate));
807        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
808    }
809
810    #[test]
811    fn steel_domain_argument_uses_server_resource_suggestions() {
812        let (domain, suggestions) = SteelArgumentType::domain().protocol_argument();
813        assert!(matches!(domain, ProtocolArgumentType::ResourceLocation));
814        assert!(matches!(suggestions, Some(SuggestionType::AskServer)));
815    }
816
817    #[test]
818    fn summonable_entity_argument_projects_vanillas_resource_and_suggestions() {
819        let (entity, suggestions) = SteelArgumentType::summonable_entity().protocol_argument();
820        assert!(matches!(
821            entity,
822            ProtocolArgumentType::Resource {
823                identifier: "minecraft:entity_type"
824            }
825        ));
826        assert!(matches!(
827            suggestions,
828            Some(SuggestionType::SummonableEntities)
829        ));
830    }
831
832    #[test]
833    fn steel_clock_arguments_project_vanilla_resource_parsers() {
834        let (clock, clock_suggestions) = SteelArgumentType::world_clock().protocol_argument();
835        assert!(matches!(
836            clock,
837            ProtocolArgumentType::Resource {
838                identifier: "minecraft:world_clock"
839            }
840        ));
841        assert!(clock_suggestions.is_none());
842
843        let (timeline, timeline_suggestions) =
844            SteelArgumentType::timeline(Some("clock")).protocol_argument();
845        assert!(matches!(
846            timeline,
847            ProtocolArgumentType::Resource {
848                identifier: "minecraft:timeline"
849            }
850        ));
851        assert!(matches!(
852            timeline_suggestions,
853            Some(SuggestionType::AskServer)
854        ));
855
856        let (marker, marker_suggestions) = SteelArgumentType::time_marker(None).protocol_argument();
857        assert!(matches!(marker, ProtocolArgumentType::ResourceLocation));
858        assert!(matches!(
859            marker_suggestions,
860            Some(SuggestionType::AskServer)
861        ));
862    }
863}