1pub 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}