steel_core/command/builtins/
fly.rs1use std::{slice, sync::Arc};
4
5use steel_utils::{Identifier, translations};
6use text_components::TextComponent;
7
8use super::super::{
9 brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
10 execution::{
11 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
12 literal,
13 },
14 registration::CommandRegistration,
15};
16use crate::player::{Abilities, DEFAULT_FLYING_SPEED, Player};
17
18const MAX_FLY_SPEED_MULTIPLIER: f32 = 30.0;
19
20pub(super) fn registration() -> CommandRegistration<CommandSource> {
21 CommandRegistration::new(Identifier::from_steel("fly"), |_| command())
22}
23
24fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
25 literal("fly")
26 .executes(toggle_sender_flight)
27 .then(
28 literal("target").then(
29 argument("targets", SteelArgumentType::players())
30 .executes(toggle_target_flight)
31 .then(argument("value", ArgumentType::bool()).executes(set_target_flight))
32 .then(
33 literal("speed")
34 .executes(query_target_flying_speed)
35 .then(speed_argument().executes(set_target_flying_speed)),
36 ),
37 ),
38 )
39 .then(
40 literal("speed")
41 .executes(query_sender_flying_speed)
42 .then(speed_argument().executes(set_sender_flying_speed)),
43 )
44}
45
46fn speed_argument() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
47 argument("speed", ArgumentType::float(0.0, MAX_FLY_SPEED_MULTIPLIER))
48}
49
50fn toggle_sender_flight(
51 context: &SteelCommandContext<CommandSource>,
52) -> Result<i32, CommandSyntaxError> {
53 let player = source_player(context)?;
54 toggle_flight(slice::from_ref(player));
55 Ok(1)
56}
57
58fn toggle_target_flight(
59 context: &SteelCommandContext<CommandSource>,
60) -> Result<i32, CommandSyntaxError> {
61 let targets = context.players("targets")?;
62 toggle_flight(&targets);
63 Ok(1)
64}
65
66fn set_target_flight(
67 context: &SteelCommandContext<CommandSource>,
68) -> Result<i32, CommandSyntaxError> {
69 let targets = context.players("targets")?;
70 let Some(allowed) = context.boolean("value") else {
71 return Err(missing_argument("value"));
72 };
73 set_flight(&targets, allowed);
74 Ok(1)
75}
76
77fn query_target_flying_speed(
78 context: &SteelCommandContext<CommandSource>,
79) -> Result<i32, CommandSyntaxError> {
80 let targets = context.players("targets")?;
81 query_flying_speed(context.source(), &targets);
82 Ok(1)
83}
84
85fn set_target_flying_speed(
86 context: &SteelCommandContext<CommandSource>,
87) -> Result<i32, CommandSyntaxError> {
88 let targets = context.players("targets")?;
89 let multiplier = required_speed(context)?;
90 set_flying_speed(context.source(), &targets, multiplier);
91 Ok(1)
92}
93
94fn query_sender_flying_speed(
95 context: &SteelCommandContext<CommandSource>,
96) -> Result<i32, CommandSyntaxError> {
97 let player = source_player(context)?;
98 query_flying_speed(context.source(), slice::from_ref(player));
99 Ok(1)
100}
101
102fn set_sender_flying_speed(
103 context: &SteelCommandContext<CommandSource>,
104) -> Result<i32, CommandSyntaxError> {
105 let player = source_player(context)?;
106 let multiplier = required_speed(context)?;
107 set_flying_speed(context.source(), slice::from_ref(player), multiplier);
108 Ok(1)
109}
110
111fn source_player(
112 context: &SteelCommandContext<CommandSource>,
113) -> Result<&Arc<Player>, CommandSyntaxError> {
114 context.source().player().ok_or_else(|| {
115 CommandSyntaxError::dynamic(TextComponent::from(
116 &translations::PERMISSIONS_REQUIRES_PLAYER,
117 ))
118 })
119}
120
121fn required_speed(context: &SteelCommandContext<CommandSource>) -> Result<f32, CommandSyntaxError> {
122 context
123 .float("speed")
124 .ok_or_else(|| missing_argument("speed"))
125}
126
127fn toggle_flight(targets: &[Arc<Player>]) {
128 for target in targets {
129 {
130 let mut abilities = target.abilities.lock();
131 let allowed = !abilities.may_fly;
132 set_flight_allowed(&mut abilities, allowed);
133 }
134 target.send_abilities();
135 }
136}
137
138fn set_flight(targets: &[Arc<Player>], allowed: bool) {
139 for target in targets {
140 {
141 let mut abilities = target.abilities.lock();
142 set_flight_allowed(&mut abilities, allowed);
143 }
144 target.send_abilities();
145 }
146}
147
148const fn set_flight_allowed(abilities: &mut Abilities, allowed: bool) {
149 abilities.may_fly = allowed;
150 if !allowed {
151 abilities.flying = false;
152 }
153}
154
155fn set_flying_speed(source: &CommandSource, targets: &[Arc<Player>], multiplier: f32) {
156 let speed = speed_from_multiplier(multiplier);
157 for target in targets {
158 target.set_flying_speed(speed);
159 target.send_abilities();
160 source.send_success(
161 &TextComponent::plain(format!(
162 "Set flying speed for player '{}' to {multiplier:.1}x ({speed:.3})",
163 target.gameprofile.name
164 )),
165 true,
166 );
167 }
168}
169
170fn query_flying_speed(source: &CommandSource, targets: &[Arc<Player>]) {
171 for target in targets {
172 let speed = target.get_flying_speed();
173 let multiplier = speed / DEFAULT_FLYING_SPEED;
174 source.send_success(
175 &TextComponent::plain(format!(
176 "Current flying speed for player '{}': {multiplier:.1}x ({speed:.3})",
177 target.gameprofile.name
178 )),
179 false,
180 );
181 }
182}
183
184fn speed_from_multiplier(multiplier: f32) -> f32 {
185 multiplier * DEFAULT_FLYING_SPEED
186}
187
188fn missing_argument(name: &str) -> CommandSyntaxError {
189 CommandSyntaxError::dynamic(format!(
190 "Parsed value for {name} is missing from the command context"
191 ))
192}
193
194#[cfg(test)]
195mod tests {
196 use super::super::create_dispatcher;
197 use super::{MAX_FLY_SPEED_MULTIPLIER, set_flight_allowed, speed_from_multiplier};
198 use crate::{
199 command::{
200 brigadier::{ArgumentType, CommandDispatcher, NodeId},
201 execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
202 },
203 player::{Abilities, DEFAULT_FLYING_SPEED},
204 };
205 use steel_registry::init_vanilla_registry;
206
207 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
208
209 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
210 let Some(children) = dispatcher.children(parent) else {
211 panic!("parent node should exist");
212 };
213 let Some(child) = children.iter().copied().find(|child| {
214 dispatcher
215 .node(*child)
216 .is_some_and(|node| node.name() == name)
217 }) else {
218 panic!("child {name} should exist");
219 };
220 child
221 }
222
223 #[test]
224 fn fly_graph_uses_explicit_target_and_bounded_speed_branches() {
225 init_vanilla_registry();
226 let Ok(dispatcher) = create_dispatcher() else {
227 panic!("built-in commands should register");
228 };
229 let fly = child(&dispatcher, dispatcher.root(), "fly");
230 assert!(matches!(
231 dispatcher.node(fly),
232 Some(node) if node.is_executable()
233 ));
234
235 let target = child(&dispatcher, fly, "target");
236 let targets = child(&dispatcher, target, "targets");
237 assert!(matches!(
238 dispatcher.node(targets),
239 Some(node)
240 if node.is_executable()
241 && node.argument_type() == Some(&SteelArgumentType::players())
242 ));
243 let value = child(&dispatcher, targets, "value");
244 assert_eq!(
245 dispatcher.node(value).and_then(|node| node.argument_type()),
246 Some(&SteelArgumentType::from(ArgumentType::bool()))
247 );
248
249 let own_speed = child(&dispatcher, fly, "speed");
250 let own_speed_value = child(&dispatcher, own_speed, "speed");
251 let target_speed = child(&dispatcher, targets, "speed");
252 let target_speed_value = child(&dispatcher, target_speed, "speed");
253 let expected = SteelArgumentType::from(ArgumentType::float(0.0, MAX_FLY_SPEED_MULTIPLIER));
254 for node in [own_speed_value, target_speed_value] {
255 assert_eq!(
256 dispatcher.node(node).and_then(|node| node.argument_type()),
257 Some(&expected)
258 );
259 assert!(matches!(
260 dispatcher.node(node),
261 Some(node) if node.is_executable()
262 ));
263 }
264 }
265
266 #[test]
267 fn disabling_flight_clears_active_flying_state() {
268 let mut abilities = Abilities {
269 may_fly: true,
270 flying: true,
271 ..Abilities::default()
272 };
273
274 set_flight_allowed(&mut abilities, false);
275 assert!(!abilities.may_fly);
276 assert!(!abilities.flying);
277
278 set_flight_allowed(&mut abilities, true);
279 assert!(abilities.may_fly);
280 assert!(!abilities.flying);
281 }
282
283 #[test]
284 fn fly_speed_uses_vanilla_default_as_the_multiplier_base() {
285 let speed = speed_from_multiplier(MAX_FLY_SPEED_MULTIPLIER);
286 let expected = 30.0 * DEFAULT_FLYING_SPEED;
287 assert!((speed - expected).abs() <= f32::EPSILON);
288 }
289}