steel_core/command/builtins/
spawnpoint.rs1use std::{slice, sync::Arc};
4
5use steel_utils::{BlockPos, Identifier, java::float_to_string, translations};
6use text_components::TextComponent;
7
8use super::super::{
9 brigadier::{CommandNodeBuilder, CommandSyntaxError},
10 execution::{
11 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
12 literal,
13 },
14 registration::CommandRegistration,
15};
16use crate::{
17 entity::Entity as _,
18 level_data::RespawnData,
19 player::{Player, PlayerRespawnConfig},
20};
21
22pub(super) fn registration() -> CommandRegistration<CommandSource> {
23 CommandRegistration::new(Identifier::vanilla_static("spawnpoint"), |_| command())
24}
25
26fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
27 literal("spawnpoint").executes(set_source_spawn).then(
28 argument("targets", SteelArgumentType::players())
29 .executes(set_source_position)
30 .then(
31 argument("pos", SteelArgumentType::block_pos())
32 .executes(set_target_position)
33 .then(
34 argument("rotation", SteelArgumentType::rotation())
35 .executes(set_target_position_and_rotation),
36 ),
37 ),
38 )
39}
40
41fn set_source_spawn(
42 context: &SteelCommandContext<CommandSource>,
43) -> Result<i32, CommandSyntaxError> {
44 let Some(player) = context.source().player() else {
45 return Err(CommandSyntaxError::dynamic(TextComponent::from(
46 &translations::PERMISSIONS_REQUIRES_PLAYER,
47 )));
48 };
49 set_spawn(
50 context,
51 slice::from_ref(player),
52 BlockPos::from(context.source().position()),
53 (0.0, 0.0),
54 )
55}
56
57fn set_source_position(
58 context: &SteelCommandContext<CommandSource>,
59) -> Result<i32, CommandSyntaxError> {
60 let targets = context.players("targets")?;
61 set_spawn(
62 context,
63 &targets,
64 BlockPos::from(context.source().position()),
65 (0.0, 0.0),
66 )
67}
68
69fn set_target_position(
70 context: &SteelCommandContext<CommandSource>,
71) -> Result<i32, CommandSyntaxError> {
72 let targets = context.players("targets")?;
73 let position = super::setworldspawn::spawnable_position(context)?;
74 set_spawn(context, &targets, position, (0.0, 0.0))
75}
76
77fn set_target_position_and_rotation(
78 context: &SteelCommandContext<CommandSource>,
79) -> Result<i32, CommandSyntaxError> {
80 let targets = context.players("targets")?;
81 let position = super::setworldspawn::spawnable_position(context)?;
82 let rotation = context.coordinates("rotation")?.rotation(context.source());
83 set_spawn(context, &targets, position, rotation)
84}
85
86fn set_spawn(
87 context: &SteelCommandContext<CommandSource>,
88 targets: &[Arc<Player>],
89 position: BlockPos,
90 (yaw, pitch): (f32, f32),
91) -> Result<i32, CommandSyntaxError> {
92 let source = context.source();
93 let respawn_data = RespawnData::of(source.world().key.clone(), position, yaw, pitch);
94 let config = PlayerRespawnConfig::new(respawn_data.clone(), true);
95
96 for target in targets {
97 target.set_respawn_position(Some(config.clone()), false);
98 }
99
100 let message = if let [target] = targets {
101 translations::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE
102 .message([
103 TextComponent::from(position.x().to_string()),
104 TextComponent::from(position.y().to_string()),
105 TextComponent::from(position.z().to_string()),
106 TextComponent::from(float_to_string(respawn_data.yaw)),
107 TextComponent::from(float_to_string(respawn_data.pitch)),
108 TextComponent::from(source.world().key.to_string()),
109 target.display_name(),
110 ])
111 .component()
112 } else {
113 translations::COMMANDS_SPAWNPOINT_SUCCESS_MULTIPLE
114 .message([
115 TextComponent::from(position.x().to_string()),
116 TextComponent::from(position.y().to_string()),
117 TextComponent::from(position.z().to_string()),
118 TextComponent::from(float_to_string(respawn_data.yaw)),
119 TextComponent::from(float_to_string(respawn_data.pitch)),
120 TextComponent::from(source.world().key.to_string()),
121 TextComponent::from(targets.len().to_string()),
122 ])
123 .component()
124 };
125 source.send_success(&message, true);
126
127 i32::try_from(targets.len()).map_err(|_| {
128 CommandSyntaxError::dynamic("Target player count exceeds the command result range")
129 })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::super::create_dispatcher;
135 use crate::command::{
136 brigadier::{CommandDispatcher, NodeId},
137 execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
138 };
139 use steel_registry::init_vanilla_registry;
140
141 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
142
143 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
144 let Some(children) = dispatcher.children(parent) else {
145 panic!("parent node should exist");
146 };
147 let Some(child) = children.iter().copied().find(|child| {
148 dispatcher
149 .node(*child)
150 .is_some_and(|node| node.name() == name)
151 }) else {
152 panic!("child {name} should exist");
153 };
154 child
155 }
156
157 fn assert_executable(dispatcher: &Dispatcher, node: NodeId) {
158 let Some(node) = dispatcher.node(node) else {
159 panic!("command node should exist");
160 };
161 assert!(node.is_executable());
162 }
163
164 #[test]
165 fn spawnpoint_graph_matches_vanilla_argument_paths() {
166 init_vanilla_registry();
167 let Ok(dispatcher) = create_dispatcher() else {
168 panic!("built-in commands should register");
169 };
170 let root = child(&dispatcher, dispatcher.root(), "spawnpoint");
171 assert_executable(&dispatcher, root);
172
173 let targets = child(&dispatcher, root, "targets");
174 assert_eq!(
175 dispatcher
176 .node(targets)
177 .and_then(|node| node.argument_type()),
178 Some(&SteelArgumentType::players())
179 );
180 assert_executable(&dispatcher, targets);
181
182 let position = child(&dispatcher, targets, "pos");
183 assert_eq!(
184 dispatcher
185 .node(position)
186 .and_then(|node| node.argument_type()),
187 Some(&SteelArgumentType::block_pos())
188 );
189 assert_executable(&dispatcher, position);
190
191 let rotation = child(&dispatcher, position, "rotation");
192 assert_eq!(
193 dispatcher
194 .node(rotation)
195 .and_then(|node| node.argument_type()),
196 Some(&SteelArgumentType::rotation())
197 );
198 assert_executable(&dispatcher, rotation);
199 }
200}