Skip to main content

steel_core/command/builtins/
mod.rs

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