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