steel_core/command/builtins/
setworldspawn.rs1use 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}