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 allowed = context.boolean("value")?;
71 set_flight(&targets, allowed);
72 Ok(1)
73}
74
75fn query_target_flying_speed(
76 context: &SteelCommandContext<CommandSource>,
77) -> Result<i32, CommandSyntaxError> {
78 let targets = context.players("targets")?;
79 query_flying_speed(context.source(), &targets);
80 Ok(1)
81}
82
83fn set_target_flying_speed(
84 context: &SteelCommandContext<CommandSource>,
85) -> Result<i32, CommandSyntaxError> {
86 let targets = context.players("targets")?;
87 let multiplier = required_speed(context)?;
88 set_flying_speed(context.source(), &targets, multiplier);
89 Ok(1)
90}
91
92fn query_sender_flying_speed(
93 context: &SteelCommandContext<CommandSource>,
94) -> Result<i32, CommandSyntaxError> {
95 let player = source_player(context)?;
96 query_flying_speed(context.source(), slice::from_ref(player));
97 Ok(1)
98}
99
100fn set_sender_flying_speed(
101 context: &SteelCommandContext<CommandSource>,
102) -> Result<i32, CommandSyntaxError> {
103 let player = source_player(context)?;
104 let multiplier = required_speed(context)?;
105 set_flying_speed(context.source(), slice::from_ref(player), multiplier);
106 Ok(1)
107}
108
109fn source_player(
110 context: &SteelCommandContext<CommandSource>,
111) -> Result<&Arc<Player>, CommandSyntaxError> {
112 context.source().player().ok_or_else(|| {
113 CommandSyntaxError::dynamic(TextComponent::from(
114 &translations::PERMISSIONS_REQUIRES_PLAYER,
115 ))
116 })
117}
118
119fn required_speed(context: &SteelCommandContext<CommandSource>) -> Result<f32, CommandSyntaxError> {
120 context.float("speed")
121}
122
123fn toggle_flight(targets: &[Arc<Player>]) {
124 for target in targets {
125 {
126 let mut abilities = target.abilities.lock();
127 let allowed = !abilities.may_fly;
128 set_flight_allowed(&mut abilities, allowed);
129 }
130 target.send_abilities();
131 }
132}
133
134fn set_flight(targets: &[Arc<Player>], allowed: bool) {
135 for target in targets {
136 {
137 let mut abilities = target.abilities.lock();
138 set_flight_allowed(&mut abilities, allowed);
139 }
140 target.send_abilities();
141 }
142}
143
144const fn set_flight_allowed(abilities: &mut Abilities, allowed: bool) {
145 abilities.may_fly = allowed;
146 if !allowed {
147 abilities.flying = false;
148 }
149}
150
151fn set_flying_speed(source: &CommandSource, targets: &[Arc<Player>], multiplier: f32) {
152 let speed = speed_from_multiplier(multiplier);
153 for target in targets {
154 target.set_flying_speed(speed);
155 target.send_abilities();
156 source.send_success(
157 &TextComponent::plain(format!(
158 "Set flying speed for player '{}' to {multiplier:.1}x ({speed:.3})",
159 target.gameprofile.name
160 )),
161 true,
162 );
163 }
164}
165
166fn query_flying_speed(source: &CommandSource, targets: &[Arc<Player>]) {
167 for target in targets {
168 let speed = target.get_flying_speed();
169 let multiplier = speed / DEFAULT_FLYING_SPEED;
170 source.send_success(
171 &TextComponent::plain(format!(
172 "Current flying speed for player '{}': {multiplier:.1}x ({speed:.3})",
173 target.gameprofile.name
174 )),
175 false,
176 );
177 }
178}
179
180fn speed_from_multiplier(multiplier: f32) -> f32 {
181 multiplier * DEFAULT_FLYING_SPEED
182}
183
184#[cfg(test)]
185mod tests {
186 use super::super::create_dispatcher;
187 use super::{MAX_FLY_SPEED_MULTIPLIER, set_flight_allowed, speed_from_multiplier};
188 use crate::{
189 command::{
190 brigadier::{ArgumentType, CommandDispatcher, NodeId},
191 execution::{CommandSource, SteelArgumentType, SteelCommandRuntime},
192 },
193 player::{Abilities, DEFAULT_FLYING_SPEED},
194 };
195 use steel_registry::init_vanilla_registry;
196
197 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
198
199 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
200 let Some(children) = dispatcher.children(parent) else {
201 panic!("parent node should exist");
202 };
203 let Some(child) = children.iter().copied().find(|child| {
204 dispatcher
205 .node(*child)
206 .is_some_and(|node| node.name() == name)
207 }) else {
208 panic!("child {name} should exist");
209 };
210 child
211 }
212
213 #[test]
214 fn fly_graph_uses_explicit_target_and_bounded_speed_branches() {
215 init_vanilla_registry();
216 let Ok(dispatcher) = create_dispatcher() else {
217 panic!("built-in commands should register");
218 };
219 let fly = child(&dispatcher, dispatcher.root(), "fly");
220 assert!(matches!(
221 dispatcher.node(fly),
222 Some(node) if node.is_executable()
223 ));
224
225 let target = child(&dispatcher, fly, "target");
226 let targets = child(&dispatcher, target, "targets");
227 assert!(matches!(
228 dispatcher.node(targets),
229 Some(node)
230 if node.is_executable()
231 && node.argument_type() == Some(&SteelArgumentType::players())
232 ));
233 let value = child(&dispatcher, targets, "value");
234 assert_eq!(
235 dispatcher.node(value).and_then(|node| node.argument_type()),
236 Some(&SteelArgumentType::from(ArgumentType::bool()))
237 );
238
239 let own_speed = child(&dispatcher, fly, "speed");
240 let own_speed_value = child(&dispatcher, own_speed, "speed");
241 let target_speed = child(&dispatcher, targets, "speed");
242 let target_speed_value = child(&dispatcher, target_speed, "speed");
243 let expected = SteelArgumentType::from(ArgumentType::float(0.0, MAX_FLY_SPEED_MULTIPLIER));
244 for node in [own_speed_value, target_speed_value] {
245 assert_eq!(
246 dispatcher.node(node).and_then(|node| node.argument_type()),
247 Some(&expected)
248 );
249 assert!(matches!(
250 dispatcher.node(node),
251 Some(node) if node.is_executable()
252 ));
253 }
254 }
255
256 #[test]
257 fn disabling_flight_clears_active_flying_state() {
258 let mut abilities = Abilities {
259 may_fly: true,
260 flying: true,
261 ..Abilities::default()
262 };
263
264 set_flight_allowed(&mut abilities, false);
265 assert!(!abilities.may_fly);
266 assert!(!abilities.flying);
267
268 set_flight_allowed(&mut abilities, true);
269 assert!(abilities.may_fly);
270 assert!(!abilities.flying);
271 }
272
273 #[test]
274 fn fly_speed_uses_vanilla_default_as_the_multiplier_base() {
275 let speed = speed_from_multiplier(MAX_FLY_SPEED_MULTIPLIER);
276 let expected = 30.0 * DEFAULT_FLYING_SPEED;
277 assert!((speed - expected).abs() <= f32::EPSILON);
278 }
279}