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 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    // TODO: Add the vanilla compound-NBT branch once Steel has an SNBT compound
41    // argument and recursive command entity loading.
42}
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}