Skip to main content

steel_core/command/builtins/
mod.rs

1//! Steel-owned built-in command declarations.
2
3mod clear;
4mod damage;
5mod difficulty;
6mod domain;
7mod enchant;
8mod execute;
9mod experience;
10mod fly;
11pub(crate) mod gamemode;
12mod gamerule;
13mod give;
14mod invsee;
15mod kill;
16mod list;
17mod locate;
18mod operator;
19mod perms;
20mod return_command;
21mod seed;
22mod setblock;
23mod setworldspawn;
24mod stop;
25mod summon;
26mod teleport;
27mod tellraw;
28mod tick;
29mod time;
30mod weather;
31
32pub(crate) use difficulty::player_can_change_difficulty;
33
34use super::{
35    CommandRegistry,
36    execution::CommandSource,
37    registration::{
38        CommandDispatcherBuilder, CommandRegistrationError,
39        ENTITY_SELECTOR_ADVANCED_PERMISSION_KEY, ENTITY_SELECTOR_PERMISSION_KEY,
40        RegisteredCommandDispatcher,
41    },
42};
43#[cfg(test)]
44use super::{brigadier::CommandDispatcher, execution::SteelCommandRuntime};
45
46#[cfg(test)]
47pub(crate) fn create_dispatcher()
48-> Result<CommandDispatcher<CommandSource, SteelCommandRuntime>, CommandRegistrationError> {
49    create_registered_dispatcher(CommandRegistry::new()).map(|registered| registered.dispatcher)
50}
51
52pub(crate) fn create_registered_dispatcher(
53    extension_commands: CommandRegistry,
54) -> Result<RegisteredCommandDispatcher<CommandSource>, CommandRegistrationError> {
55    let mut builder = CommandDispatcherBuilder::new();
56    builder.declare_permission(ENTITY_SELECTOR_PERMISSION_KEY)?;
57    builder.declare_permission(ENTITY_SELECTOR_ADVANCED_PERMISSION_KEY)?;
58    builder.declare_permission(perms::MANAGE_ALL_PERMISSION)?;
59    builder.declare_permission(perms::GROUP_ALL_PERMISSION)?;
60    builder.declare_permission(perms::METADATA_PERMISSION)?;
61    builder.register(clear::registration())?;
62    builder.register(operator::deop_registration())?;
63    builder.register(damage::registration())?;
64    builder.register(difficulty::registration())?;
65    builder.register(domain::registration())?;
66    builder.register(enchant::registration())?;
67    builder.register(execute::registration())?;
68    builder.register(experience::registration())?;
69    builder.register(fly::registration())?;
70    builder.register(gamemode::registration()?)?;
71    builder.register(gamerule::registration())?;
72    builder.register(give::registration())?;
73    builder.register(kill::registration())?;
74    builder.register(list::registration())?;
75    builder.register(locate::registration())?;
76    builder.register(operator::op_registration())?;
77    builder.register(perms::registration())?;
78    builder.register(return_command::registration())?;
79    builder.register(seed::registration())?;
80    builder.register(setblock::registration())?;
81    builder.register(setworldspawn::registration())?;
82    builder.register(stop::registration())?;
83    builder.register(summon::registration())?;
84    builder.register(teleport::registration())?;
85    builder.register(tellraw::registration())?;
86    builder.register(tick::registration())?;
87    builder.register(time::registration())?;
88    builder.register(weather::registration())?;
89    builder.register(invsee::registration()?)?;
90    builder.extend(extension_commands.into_inner())?;
91    builder.build_with_permissions()
92}
93
94#[cfg(test)]
95mod tests {
96    use super::{create_dispatcher, create_registered_dispatcher};
97    use crate::command::brigadier::ArgumentType;
98    use crate::command::execution::SteelArgumentType;
99    use crate::command::{
100        CommandRegistration as ExtensionCommandRegistration, CommandRegistry,
101        literal as extension_literal,
102    };
103    use crate::permission::PermissionKey;
104    use steel_registry::init_vanilla_registry;
105    use steel_utils::Identifier;
106
107    #[expect(
108        clippy::too_many_lines,
109        reason = "one cohesive test keeps the expected command graph shape auditable"
110    )]
111    #[test]
112    fn first_builtin_slice_has_the_expected_graph_shape() {
113        init_vanilla_registry();
114        let Ok(dispatcher) = create_dispatcher() else {
115            panic!("built-in commands should register");
116        };
117        let Some(roots) = dispatcher.children(dispatcher.root()) else {
118            panic!("dispatcher root should exist");
119        };
120        let names = roots
121            .iter()
122            .map(|root| {
123                let Some(root) = dispatcher.node(*root) else {
124                    panic!("registered root should exist");
125                };
126                root.name()
127            })
128            .collect::<Vec<_>>();
129
130        assert_eq!(
131            names,
132            [
133                "clear",
134                "deop",
135                "damage",
136                "difficulty",
137                "domain",
138                "enchant",
139                "execute",
140                "experience",
141                "xp",
142                "fly",
143                "gamemode",
144                "gamerule",
145                "give",
146                "kill",
147                "list",
148                "locate",
149                "op",
150                "perms",
151                "return",
152                "seed",
153                "setblock",
154                "setworldspawn",
155                "stop",
156                "summon",
157                "teleport",
158                "tp",
159                "tellraw",
160                "tick",
161                "time",
162                "weather",
163                "invsee"
164            ]
165        );
166
167        let Some(list) = roots.iter().copied().find(|root| {
168            dispatcher
169                .node(*root)
170                .is_some_and(|node| node.name() == "list")
171        }) else {
172            panic!("list root should exist");
173        };
174        let Some(list_node) = dispatcher.node(list) else {
175            panic!("list root should exist");
176        };
177        assert!(list_node.is_executable());
178        let Some(list_children) = dispatcher.children(list) else {
179            panic!("list children should exist");
180        };
181        assert_eq!(list_children.len(), 1);
182        assert!(
183            dispatcher
184                .node(list_children[0])
185                .is_some_and(|node| { node.name() == "uuids" && node.is_executable() })
186        );
187
188        let Some(weather) = roots.iter().copied().find(|root| {
189            dispatcher
190                .node(*root)
191                .is_some_and(|node| node.name() == "weather")
192        }) else {
193            panic!("weather root should exist");
194        };
195        let Some(weather_children) = dispatcher.children(weather) else {
196            panic!("weather children should exist");
197        };
198        let weather_names = weather_children
199            .iter()
200            .map(|child| {
201                let Some(node) = dispatcher.node(*child) else {
202                    panic!("weather literal should exist");
203                };
204                assert!(node.is_executable());
205                let Some(duration_children) = dispatcher.children(*child) else {
206                    panic!("weather duration child should exist");
207                };
208                assert_eq!(duration_children.len(), 1);
209                let Some(duration) = dispatcher.node(duration_children[0]) else {
210                    panic!("weather duration node should exist");
211                };
212                assert_eq!(duration.name(), "duration");
213                assert!(duration.is_executable());
214                assert_eq!(duration.argument_type(), Some(&SteelArgumentType::time(1)));
215                node.name()
216            })
217            .collect::<Vec<_>>();
218        assert_eq!(weather_names, ["clear", "rain", "thunder"]);
219    }
220
221    #[test]
222    fn startup_extensions_merge_after_builtins_with_namespaced_collision_fallbacks() {
223        init_vanilla_registry();
224        let mut extensions = CommandRegistry::new();
225        let registration =
226            ExtensionCommandRegistration::new(Identifier::new("steel_test", "stop"), || {
227                extension_literal("stop").executes(|_| Ok(1))
228            });
229        assert!(extensions.register(registration).is_ok());
230
231        let Ok(registered) = create_registered_dispatcher(extensions) else {
232            panic!("built-ins and extension should register atomically");
233        };
234        let dispatcher = registered.dispatcher;
235        let Some(roots) = dispatcher.children(dispatcher.root()) else {
236            panic!("dispatcher root should exist");
237        };
238        let names = roots
239            .iter()
240            .filter_map(|root| {
241                let node = dispatcher.node(*root)?;
242                Some(node.name())
243            })
244            .collect::<Vec<_>>();
245
246        assert!(names.contains(&"stop"));
247        assert!(names.contains(&"steel_test:stop"));
248        assert!(
249            registered
250                .permissions
251                .iter()
252                .any(|permission| permission.as_str() == "steel_test.command.stop")
253        );
254    }
255
256    #[test]
257    fn invsee_discovers_only_steel_namespaced_permissions() {
258        init_vanilla_registry();
259        let Ok(registered) = create_registered_dispatcher(CommandRegistry::new()) else {
260            panic!("built-in commands should register");
261        };
262        let permissions = registered
263            .permissions
264            .iter()
265            .map(PermissionKey::as_str)
266            .collect::<Vec<_>>();
267
268        assert!(permissions.contains(&"steel.command.invsee"));
269        assert!(permissions.contains(&"steel.command.invsee.modify"));
270        assert!(!permissions.contains(&"minecraft.command.invsee"));
271    }
272
273    #[test]
274    #[expect(
275        clippy::too_many_lines,
276        reason = "one graph-shape test keeps execute paths and redirects directly comparable"
277    )]
278    fn execute_graph_uses_expected_redirects_and_argument_types() {
279        init_vanilla_registry();
280        let Ok(dispatcher) = create_dispatcher() else {
281            panic!("built-in commands should register");
282        };
283        let Some(execute) = dispatcher.children(dispatcher.root()).and_then(|children| {
284            children.iter().copied().find(|child| {
285                dispatcher
286                    .node(*child)
287                    .is_some_and(|node| node.name() == "execute")
288            })
289        }) else {
290            panic!("execute root should exist");
291        };
292        let Some(execute_node) = dispatcher.node(execute) else {
293            panic!("execute root should exist");
294        };
295        assert!(!execute_node.is_executable());
296
297        let child = |parent, name| {
298            let Some(node) = dispatcher.children(parent).and_then(|children| {
299                children.iter().copied().find(|child| {
300                    dispatcher
301                        .node(*child)
302                        .is_some_and(|node| node.name() == name)
303                })
304            }) else {
305                panic!("{name} should exist below {parent:?}");
306            };
307            node
308        };
309
310        let run = child(execute, "run");
311        let Some(run_node) = dispatcher.node(run) else {
312            panic!("execute run should exist");
313        };
314        assert_eq!(run_node.redirect(), Some(dispatcher.root()));
315
316        for condition in ["if", "unless"] {
317            for (path, expected_type) in [
318                (
319                    [condition, "entity", "entities"],
320                    SteelArgumentType::entities(),
321                ),
322                ([condition, "loaded", "pos"], SteelArgumentType::block_pos()),
323            ] {
324                let terminal = path
325                    .iter()
326                    .fold(execute, |parent, name| child(parent, name));
327                let Some(node) = dispatcher.node(terminal) else {
328                    panic!("execute condition terminal should exist");
329                };
330                assert!(node.is_executable());
331                assert_eq!(node.redirect(), Some(execute));
332                assert_eq!(node.argument_type(), Some(&expected_type));
333            }
334
335            let biome = child(child(child(execute, condition), "biome"), "pos");
336            let biome = child(biome, "biome");
337            let Some(biome_node) = dispatcher.node(biome) else {
338                panic!("execute biome condition terminal should exist");
339            };
340            assert!(biome_node.is_executable());
341            assert_eq!(biome_node.redirect(), Some(execute));
342            assert_eq!(
343                biome_node.argument_type(),
344                Some(&SteelArgumentType::biome_or_tag())
345            );
346
347            let block = child(child(child(execute, condition), "block"), "pos");
348            let block = child(block, "block");
349            let Some(block_node) = dispatcher.node(block) else {
350                panic!("execute block condition terminal should exist");
351            };
352            assert!(block_node.is_executable());
353            assert_eq!(block_node.redirect(), Some(execute));
354            assert_eq!(
355                block_node.argument_type(),
356                Some(&SteelArgumentType::block_predicate())
357            );
358
359            let blocks = child(child(execute, condition), "blocks");
360            let start = child(blocks, "start");
361            let end = child(start, "end");
362            let destination = child(end, "destination");
363            for position in [start, end, destination] {
364                assert_eq!(
365                    dispatcher
366                        .node(position)
367                        .and_then(|node| node.argument_type()),
368                    Some(&SteelArgumentType::block_pos())
369                );
370            }
371            for mode in ["all", "masked"] {
372                let terminal = child(destination, mode);
373                let Some(node) = dispatcher.node(terminal) else {
374                    panic!("execute blocks mode should exist");
375                };
376                assert!(node.is_executable());
377                assert_eq!(node.redirect(), Some(execute));
378            }
379
380            let data = child(child(execute, condition), "data");
381            for (provider, source_name, source_type) in [
382                ("block", "sourcePos", SteelArgumentType::block_pos()),
383                ("entity", "source", SteelArgumentType::entity()),
384                ("storage", "source", SteelArgumentType::storage_key()),
385            ] {
386                let source = child(child(data, provider), source_name);
387                assert_eq!(
388                    dispatcher
389                        .node(source)
390                        .and_then(|node| node.argument_type()),
391                    Some(&source_type)
392                );
393                let path = child(source, "path");
394                let Some(node) = dispatcher.node(path) else {
395                    panic!("execute data path terminal should exist");
396                };
397                assert!(node.is_executable());
398                assert_eq!(node.redirect(), Some(execute));
399                assert_eq!(node.argument_type(), Some(&SteelArgumentType::nbt_path()));
400            }
401
402            let dimension = child(child(execute, condition), "dimension");
403            let dimension = child(dimension, "dimension");
404            let Some(node) = dispatcher.node(dimension) else {
405                panic!("execute dimension condition terminal should exist");
406            };
407            assert!(node.is_executable());
408            assert_eq!(node.redirect(), Some(execute));
409            assert_eq!(node.argument_type(), Some(&SteelArgumentType::world()));
410
411            let score = child(child(execute, condition), "score");
412            let target = child(score, "target");
413            assert_eq!(
414                dispatcher
415                    .node(target)
416                    .and_then(|node| node.argument_type()),
417                Some(&SteelArgumentType::score_holder())
418            );
419            let target_objective = child(target, "targetObjective");
420            assert_eq!(
421                dispatcher
422                    .node(target_objective)
423                    .and_then(|node| node.argument_type()),
424                Some(&SteelArgumentType::objective())
425            );
426            for comparison in ["=", "<", "<=", ">", ">="] {
427                let source = child(child(target_objective, comparison), "source");
428                let source_objective = child(source, "sourceObjective");
429                let Some(node) = dispatcher.node(source_objective) else {
430                    panic!("score comparison terminal should exist");
431                };
432                assert!(node.is_executable());
433                assert_eq!(node.redirect(), Some(execute));
434                assert_eq!(node.argument_type(), Some(&SteelArgumentType::objective()));
435            }
436            let range = child(child(target_objective, "matches"), "range");
437            let Some(range_node) = dispatcher.node(range) else {
438                panic!("score range terminal should exist");
439            };
440            assert!(range_node.is_executable());
441            assert_eq!(range_node.redirect(), Some(execute));
442            assert_eq!(
443                range_node.argument_type(),
444                Some(&SteelArgumentType::int_range())
445            );
446        }
447
448        for store_kind in ["result", "success"] {
449            let store = child(child(execute, "store"), store_kind);
450            let targets = child(child(store, "score"), "targets");
451            assert_eq!(
452                dispatcher
453                    .node(targets)
454                    .and_then(|node| node.argument_type()),
455                Some(&SteelArgumentType::score_holders())
456            );
457            let objective = child(targets, "objective");
458            let Some(objective_node) = dispatcher.node(objective) else {
459                panic!("execute store score objective should exist");
460            };
461            assert_eq!(objective_node.redirect(), Some(execute));
462            assert_eq!(
463                objective_node.argument_type(),
464                Some(&SteelArgumentType::objective())
465            );
466
467            for (provider, target_name, target_type) in [
468                ("block", "targetPos", SteelArgumentType::block_pos()),
469                ("storage", "target", SteelArgumentType::storage_key()),
470            ] {
471                let target = child(child(store, provider), target_name);
472                assert_eq!(
473                    dispatcher
474                        .node(target)
475                        .and_then(|node| node.argument_type()),
476                    Some(&target_type)
477                );
478                let path = child(target, "path");
479                assert_eq!(
480                    dispatcher.node(path).and_then(|node| node.argument_type()),
481                    Some(&SteelArgumentType::nbt_path())
482                );
483                for data_type in ["int", "float", "short", "long", "double", "byte"] {
484                    let scale = child(child(path, data_type), "scale");
485                    let Some(scale_node) = dispatcher.node(scale) else {
486                        panic!("execute store data scale should exist");
487                    };
488                    assert_eq!(scale_node.redirect(), Some(execute));
489                    assert_eq!(
490                        scale_node.argument_type(),
491                        Some(&SteelArgumentType::from(ArgumentType::double(
492                            f64::MIN,
493                            f64::MAX,
494                        )))
495                    );
496                }
497            }
498        }
499
500        let modifier_paths: &[&[&str]] = &[
501            &["as", "targets"],
502            &["at", "targets"],
503            &["positioned", "pos"],
504            &["positioned", "as", "targets"],
505            &["positioned", "over", "heightmap"],
506            &["rotated", "rot"],
507            &["rotated", "as", "targets"],
508            &["facing", "pos"],
509            &["facing", "entity", "targets", "anchor"],
510            &["align", "axes"],
511            &["anchored", "anchor"],
512            &["in", "dimension"],
513            &["summon", "entity"],
514            &["on", "vehicle"],
515            &["on", "controller"],
516            &["on", "passengers"],
517        ];
518        for path in modifier_paths {
519            let terminal = path
520                .iter()
521                .fold(execute, |parent, name| child(parent, name));
522            let Some(terminal_node) = dispatcher.node(terminal) else {
523                panic!("execute modifier terminal should exist");
524            };
525            assert_eq!(
526                terminal_node.redirect(),
527                Some(execute),
528                "execute {} should redirect to the execute root",
529                path.join(" ")
530            );
531        }
532
533        let argument_types: &[(&[&str], SteelArgumentType)] = &[
534            (&["as", "targets"], SteelArgumentType::entities()),
535            (&["at", "targets"], SteelArgumentType::entities()),
536            (&["positioned", "pos"], SteelArgumentType::vec3(true)),
537            (
538                &["positioned", "over", "heightmap"],
539                SteelArgumentType::heightmap(),
540            ),
541            (&["rotated", "rot"], SteelArgumentType::rotation()),
542            (&["facing", "pos"], SteelArgumentType::vec3(true)),
543            (
544                &["facing", "entity", "targets", "anchor"],
545                SteelArgumentType::entity_anchor(),
546            ),
547            (&["align", "axes"], SteelArgumentType::swizzle()),
548            (&["anchored", "anchor"], SteelArgumentType::entity_anchor()),
549            (
550                &["summon", "entity"],
551                SteelArgumentType::summonable_entity(),
552            ),
553        ];
554        for (path, expected) in argument_types {
555            let argument = path
556                .iter()
557                .fold(execute, |parent, name| child(parent, name));
558            assert_eq!(
559                dispatcher
560                    .node(argument)
561                    .and_then(|node| node.argument_type()),
562                Some(expected),
563                "execute {} should use the expected argument parser",
564                path.join(" ")
565            );
566        }
567    }
568}