Skip to main content

steel_core/command/builtins/
fly.rs

1//! Steel player-flight command.
2
3use 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}