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