Skip to main content

steel_core/command/builtins/
summon.rs

1//! Entity summoning command.
2
3use 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 position = context.coordinates("pos")?;
34                    summon_entity(context, position.position(context.source()))
35                }),
36            ),
37    )
38    // TODO: Add the vanilla compound-NBT branch once Steel has an SNBT compound
39    // argument and recursive command entity loading.
40}
41
42fn summon_entity(
43    context: &SteelCommandContext<CommandSource>,
44    position: DVec3,
45) -> Result<i32, CommandSyntaxError> {
46    let entity_type = context.entity_type("entity")?;
47    let entity = create_entity(context, entity_type, position)?;
48    let message = translations::COMMANDS_SUMMON_SUCCESS
49        .message([entity.display_name()])
50        .component();
51    context.source().send_success(&message, true);
52    Ok(1)
53}
54
55pub(super) fn create_entity(
56    context: &SteelCommandContext<CommandSource>,
57    entity_type: EntityTypeRef,
58    position: DVec3,
59) -> Result<SharedEntity, CommandSyntaxError> {
60    if !World::is_in_spawnable_bounds(BlockPos::from(position)) {
61        return Err(command_failed(
62            &translations::COMMANDS_SUMMON_INVALID_POSITION,
63        ));
64    }
65
66    let world = context.source().world();
67    if world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful {
68        return Err(command_failed(
69            &translations::COMMANDS_SUMMON_FAILED_PEACEFUL,
70        ));
71    }
72
73    let Some(entity) = ENTITIES.create(
74        entity_type,
75        next_entity_id(),
76        position,
77        Arc::downgrade(world),
78    ) else {
79        return Err(command_failed(&translations::COMMANDS_SUMMON_FAILED));
80    };
81
82    if let Some(mob) = entity.as_mob() {
83        let _ = mob.finalize_spawn(world, EntitySpawnReason::Command, None);
84    }
85
86    match world.try_add_entity(Arc::clone(&entity)) {
87        Ok(()) => Ok(entity),
88        Err(AddEntityError::DuplicateUuid { .. }) => {
89            Err(command_failed(&translations::COMMANDS_SUMMON_FAILED_UUID))
90        }
91        Err(_) => Err(command_failed(&translations::COMMANDS_SUMMON_FAILED)),
92    }
93}
94
95fn command_failed(translation: &'static Translation<0>) -> CommandSyntaxError {
96    CommandSyntaxError::dynamic(TextComponent::from(translation))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::super::create_dispatcher;
102    use crate::bootstrap::init_globals;
103    use crate::command::{
104        brigadier::{CommandDispatcher, NodeId},
105        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
106    };
107
108    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
109
110    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
111        let Some(children) = dispatcher.children(parent) else {
112            panic!("parent node should exist");
113        };
114        let Some(child) = children.iter().copied().find(|child| {
115            dispatcher
116                .node(*child)
117                .is_some_and(|node| node.name() == name)
118        }) else {
119            panic!("child {name} should exist");
120        };
121        child
122    }
123
124    #[test]
125    fn summon_graph_uses_typed_entity_and_deferred_position_arguments() {
126        init_globals();
127        let Ok(dispatcher) = create_dispatcher() else {
128            panic!("built-in commands should register");
129        };
130        let root = child(&dispatcher, dispatcher.root(), "summon");
131        let entity = child(&dispatcher, root, "entity");
132        assert_eq!(
133            dispatcher
134                .node(entity)
135                .and_then(|node| node.argument_type()),
136            Some(&SteelArgumentType::summonable_entity())
137        );
138        assert!(matches!(
139            dispatcher.node(entity),
140            Some(node) if node.is_executable()
141        ));
142
143        let position = child(&dispatcher, entity, "pos");
144        assert_eq!(
145            dispatcher
146                .node(position)
147                .and_then(|node| node.argument_type()),
148            Some(&SteelArgumentType::vec3(true))
149        );
150        assert!(matches!(
151            dispatcher.node(position),
152            Some(node) if node.is_executable()
153        ));
154        assert!(dispatcher.children(position).is_some_and(<[_]>::is_empty));
155    }
156}