steel_core/command/builtins/
summon.rs1use std::sync::Arc;
4
5use glam::DVec3;
6use steel_registry::entity_type::EntityTypeRef;
7use steel_utils::{BlockPos, Identifier, translations, types::Difficulty};
8use text_components::{TextComponent, translation::Translation};
9
10use super::super::{
11 brigadier::{CommandNodeBuilder, CommandSyntaxError},
12 execution::{
13 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
14 literal,
15 },
16 registration::CommandRegistration,
17};
18use crate::{
19 entity::{AddEntityError, ENTITIES, EntitySpawnReason, SharedEntity, next_entity_id},
20 world::World,
21};
22
23pub(super) fn registration() -> CommandRegistration<CommandSource> {
24 CommandRegistration::new(Identifier::vanilla_static("summon"), |_| command())
25}
26
27fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
28 literal("summon").then(
29 argument("entity", SteelArgumentType::summonable_entity())
30 .executes(|context| summon_entity(context, context.source().position()))
31 .then(
32 argument("pos", SteelArgumentType::vec3(true)).executes(|context| {
33 let Some(position) = context.coordinates("pos") else {
34 return Err(missing_argument("pos"));
35 };
36 summon_entity(context, position.position(context.source()))
37 }),
38 ),
39 )
40 }
43
44fn summon_entity(
45 context: &SteelCommandContext<CommandSource>,
46 position: DVec3,
47) -> Result<i32, CommandSyntaxError> {
48 let Some(entity_type) = context.entity_type("entity") else {
49 return Err(missing_argument("entity"));
50 };
51 let entity = create_entity(context, entity_type, position)?;
52 let message = translations::COMMANDS_SUMMON_SUCCESS
53 .message([entity.display_name()])
54 .component();
55 context.source().send_success(&message, true);
56 Ok(1)
57}
58
59pub(super) fn create_entity(
60 context: &SteelCommandContext<CommandSource>,
61 entity_type: EntityTypeRef,
62 position: DVec3,
63) -> Result<SharedEntity, CommandSyntaxError> {
64 if !World::is_in_spawnable_bounds(BlockPos::from(position)) {
65 return Err(command_failed(
66 &translations::COMMANDS_SUMMON_INVALID_POSITION,
67 ));
68 }
69
70 let world = context.source().world();
71 if world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful {
72 return Err(command_failed(
73 &translations::COMMANDS_SUMMON_FAILED_PEACEFUL,
74 ));
75 }
76
77 let Some(entity) = ENTITIES.create(
78 entity_type,
79 next_entity_id(),
80 position,
81 Arc::downgrade(world),
82 ) else {
83 return Err(command_failed(&translations::COMMANDS_SUMMON_FAILED));
84 };
85
86 if let Some(mob) = entity.as_mob() {
87 let _ = mob.finalize_spawn(world, EntitySpawnReason::Command, None);
88 }
89
90 match world.try_add_entity(Arc::clone(&entity)) {
91 Ok(()) => Ok(entity),
92 Err(AddEntityError::DuplicateUuid { .. }) => {
93 Err(command_failed(&translations::COMMANDS_SUMMON_FAILED_UUID))
94 }
95 Err(_) => Err(command_failed(&translations::COMMANDS_SUMMON_FAILED)),
96 }
97}
98
99fn command_failed(translation: &'static Translation<0>) -> CommandSyntaxError {
100 CommandSyntaxError::dynamic(TextComponent::from(translation))
101}
102
103fn missing_argument(name: &str) -> CommandSyntaxError {
104 CommandSyntaxError::dynamic(format!(
105 "Parsed value for {name} is missing from the command context"
106 ))
107}
108
109#[cfg(test)]
110mod tests {
111 use super::super::create_dispatcher;
112 use crate::bootstrap::init_globals_once;
113 use crate::command::{
114 brigadier::{CommandDispatcher, NodeId},
115 execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
116 };
117
118 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
119
120 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
121 let Some(children) = dispatcher.children(parent) else {
122 panic!("parent node should exist");
123 };
124 let Some(child) = children.iter().copied().find(|child| {
125 dispatcher
126 .node(*child)
127 .is_some_and(|node| node.name() == name)
128 }) else {
129 panic!("child {name} should exist");
130 };
131 child
132 }
133
134 #[test]
135 fn summon_graph_uses_typed_entity_and_deferred_position_arguments() {
136 init_globals_once();
137 let Ok(dispatcher) = create_dispatcher() else {
138 panic!("built-in commands should register");
139 };
140 let root = child(&dispatcher, dispatcher.root(), "summon");
141 let entity = child(&dispatcher, root, "entity");
142 assert_eq!(
143 dispatcher
144 .node(entity)
145 .and_then(|node| node.argument_type()),
146 Some(&SteelArgumentType::summonable_entity())
147 );
148 assert!(matches!(
149 dispatcher.node(entity),
150 Some(node) if node.is_executable()
151 ));
152
153 let position = child(&dispatcher, entity, "pos");
154 assert_eq!(
155 dispatcher
156 .node(position)
157 .and_then(|node| node.argument_type()),
158 Some(&SteelArgumentType::vec3(true))
159 );
160 assert!(matches!(
161 dispatcher.node(position),
162 Some(node) if node.is_executable()
163 ));
164 assert!(dispatcher.children(position).is_some_and(<[_]>::is_empty));
165 }
166}