Skip to main content

steel_core/command/builtins/
setworldspawn.rs

1//! Default world spawn command.
2
3use steel_utils::{BlockPos, Identifier, translations};
4use text_components::TextComponent;
5
6use super::super::{
7    brigadier::{CommandNodeBuilder, CommandSyntaxError},
8    execution::{
9        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
10        literal,
11    },
12    registration::CommandRegistration,
13};
14use crate::{level_data::RespawnData, world::World};
15
16pub(super) fn registration() -> CommandRegistration<CommandSource> {
17    CommandRegistration::new(Identifier::vanilla_static("setworldspawn"), |_| command())
18}
19
20fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
21    literal("setworldspawn")
22        .executes(|context| {
23            set_spawn(
24                context,
25                BlockPos::from(context.source().position()),
26                (0.0, 0.0),
27            )
28        })
29        .then(
30            argument("pos", SteelArgumentType::block_pos())
31                .executes(|context| {
32                    let position = spawnable_position(context)?;
33                    set_spawn(context, position, (0.0, 0.0))
34                })
35                .then(
36                    argument("rotation", SteelArgumentType::rotation()).executes(|context| {
37                        let position = spawnable_position(context)?;
38                        let Some(rotation) = context.coordinates("rotation") else {
39                            return Err(missing_argument("rotation"));
40                        };
41                        set_spawn(context, position, rotation.rotation(context.source()))
42                    }),
43                ),
44        )
45}
46
47fn spawnable_position(
48    context: &SteelCommandContext<CommandSource>,
49) -> Result<BlockPos, CommandSyntaxError> {
50    let Some(coordinates) = context.coordinates("pos") else {
51        return Err(missing_argument("pos"));
52    };
53    let position = coordinates.block_pos(context.source());
54    if !World::is_in_spawnable_bounds(position) {
55        return Err(CommandSyntaxError::dynamic(TextComponent::from(
56            &translations::ARGUMENT_POS_OUTOFBOUNDS,
57        )));
58    }
59    Ok(position)
60}
61
62fn set_spawn(
63    context: &SteelCommandContext<CommandSource>,
64    position: BlockPos,
65    (yaw, pitch): (f32, f32),
66) -> Result<i32, CommandSyntaxError> {
67    let source = context.source();
68    let respawn_data = RespawnData::of(source.world().key.clone(), position, yaw, pitch);
69    let yaw = respawn_data.yaw;
70    let pitch = respawn_data.pitch;
71    source
72        .server()
73        .set_respawn_data(respawn_data)
74        .map_err(CommandSyntaxError::dynamic)?;
75
76    let message = translations::COMMANDS_SETWORLDSPAWN_SUCCESS_NEW
77        .message([
78            position.x().to_string(),
79            position.y().to_string(),
80            position.z().to_string(),
81            yaw.to_string(),
82            pitch.to_string(),
83            source.world().key.to_string(),
84        ])
85        .component();
86    source.send_success(&message, true);
87    Ok(1)
88}
89
90fn missing_argument(name: &str) -> CommandSyntaxError {
91    CommandSyntaxError::dynamic(format!(
92        "Parsed value for {name} is missing from the command context"
93    ))
94}
95
96#[cfg(test)]
97mod tests {
98    use super::super::create_dispatcher;
99    use crate::command::{
100        brigadier::{CommandDispatcher, NodeId},
101        execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
102    };
103    use steel_registry::init_vanilla_registry;
104
105    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
106
107    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
108        let Some(children) = dispatcher.children(parent) else {
109            panic!("parent node should exist");
110        };
111        let Some(child) = children.iter().copied().find(|child| {
112            dispatcher
113                .node(*child)
114                .is_some_and(|node| node.name() == name)
115        }) else {
116            panic!("child {name} should exist");
117        };
118        child
119    }
120
121    #[test]
122    fn setworldspawn_graph_uses_deferred_coordinate_arguments() {
123        init_vanilla_registry();
124        let Ok(dispatcher) = create_dispatcher() else {
125            panic!("built-in commands should register");
126        };
127        let root = child(&dispatcher, dispatcher.root(), "setworldspawn");
128        let Some(root_node) = dispatcher.node(root) else {
129            panic!("setworldspawn root should exist");
130        };
131        assert!(root_node.is_executable());
132
133        let position = child(&dispatcher, root, "pos");
134        assert_eq!(
135            dispatcher
136                .node(position)
137                .and_then(|node| node.argument_type()),
138            Some(&SteelArgumentType::block_pos())
139        );
140        let Some(position_node) = dispatcher.node(position) else {
141            panic!("setworldspawn position should exist");
142        };
143        assert!(position_node.is_executable());
144
145        let rotation = child(&dispatcher, position, "rotation");
146        assert_eq!(
147            dispatcher
148                .node(rotation)
149                .and_then(|node| node.argument_type()),
150            Some(&SteelArgumentType::rotation())
151        );
152        let Some(rotation_node) = dispatcher.node(rotation) else {
153            panic!("setworldspawn rotation should exist");
154        };
155        assert!(rotation_node.is_executable());
156    }
157}