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