Skip to main content

steel_core/command/builtins/
spawnpoint.rs

1//! Vanilla per-player spawn-point command.
2
3use 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}