Skip to main content

steel_core/command/builtins/
title.rs

1//! Vanilla title display command.
2
3use std::sync::Arc;
4
5use steel_utils::{Identifier, translations};
6use text_components::TextComponent;
7
8use super::super::{
9    brigadier::{CommandNodeBuilder, CommandSyntaxError},
10    execution::{
11        CommandSource, CommandTextResolver, SteelArgumentType, SteelCommandContext,
12        SteelCommandRuntime, argument, literal,
13    },
14    registration::CommandRegistration,
15};
16use crate::{entity::Entity as _, player::Player};
17
18pub(super) fn registration() -> CommandRegistration<CommandSource> {
19    CommandRegistration::new(Identifier::vanilla_static("title"), |_| command())
20}
21
22fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
23    literal("title").then(
24        argument("targets", SteelArgumentType::players())
25            .then(literal("clear").executes(clear_titles))
26            .then(literal("reset").executes(reset_titles))
27            .then(
28                literal("title").then(
29                    argument("title", SteelArgumentType::component())
30                        .executes(|context| show_title(context, TitleKind::Title)),
31                ),
32            )
33            .then(
34                literal("subtitle").then(
35                    argument("title", SteelArgumentType::component())
36                        .executes(|context| show_title(context, TitleKind::Subtitle)),
37                ),
38            )
39            .then(
40                literal("actionbar").then(
41                    argument("title", SteelArgumentType::component())
42                        .executes(|context| show_title(context, TitleKind::Actionbar)),
43                ),
44            )
45            .then(
46                literal("times").then(
47                    argument("fadeIn", SteelArgumentType::time(0)).then(
48                        argument("stay", SteelArgumentType::time(0)).then(
49                            argument("fadeOut", SteelArgumentType::time(0)).executes(set_times),
50                        ),
51                    ),
52                ),
53            ),
54    )
55}
56
57fn clear_titles(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
58    let targets = context.players("targets")?;
59    let result = target_count(&targets)?;
60    for target in &targets {
61        target.clear_titles();
62    }
63    send_success(context, &targets, TitleOperation::Clear);
64    Ok(result)
65}
66
67fn reset_titles(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
68    let targets = context.players("targets")?;
69    let result = target_count(&targets)?;
70    for target in &targets {
71        target.reset_titles();
72    }
73    send_success(context, &targets, TitleOperation::Reset);
74    Ok(result)
75}
76
77fn show_title(
78    context: &SteelCommandContext<CommandSource>,
79    kind: TitleKind,
80) -> Result<i32, CommandSyntaxError> {
81    let targets = context.players("targets")?;
82    let result = target_count(&targets)?;
83    let title = context.text_component("title")?;
84
85    for target in &targets {
86        let title = title.try_resolve(&CommandTextResolver::with_entity_override(
87            context.source(),
88            target.as_ref(),
89        ))?;
90        match kind {
91            TitleKind::Title => target.send_title(title),
92            TitleKind::Subtitle => target.send_subtitle(title),
93            TitleKind::Actionbar => target.send_action_bar(title),
94        }
95    }
96
97    send_success(context, &targets, TitleOperation::Show(kind));
98    Ok(result)
99}
100
101fn set_times(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
102    let targets = context.players("targets")?;
103    let result = target_count(&targets)?;
104    let fade_in = context.time("fadeIn")?;
105    let stay = context.time("stay")?;
106    let fade_out = context.time("fadeOut")?;
107
108    for target in &targets {
109        target.send_title_times(fade_in, stay, fade_out);
110    }
111    send_success(context, &targets, TitleOperation::Times);
112    Ok(result)
113}
114
115fn target_count(targets: &[Arc<Player>]) -> Result<i32, CommandSyntaxError> {
116    i32::try_from(targets.len()).map_err(|_| {
117        CommandSyntaxError::dynamic("Target player count exceeds the command result range")
118    })
119}
120
121fn send_success(
122    context: &SteelCommandContext<CommandSource>,
123    targets: &[Arc<Player>],
124    operation: TitleOperation,
125) {
126    let message = operation.success_message(targets);
127    context.source().send_success(&message, true);
128}
129
130#[derive(Clone, Copy)]
131enum TitleKind {
132    Title,
133    Subtitle,
134    Actionbar,
135}
136
137#[derive(Clone, Copy)]
138enum TitleOperation {
139    Clear,
140    Reset,
141    Show(TitleKind),
142    Times,
143}
144
145impl TitleOperation {
146    fn success_message(self, targets: &[Arc<Player>]) -> TextComponent {
147        if let [target] = targets {
148            let name = target.display_name();
149            return match self {
150                Self::Clear => translations::COMMANDS_TITLE_CLEARED_SINGLE
151                    .message([name])
152                    .component(),
153                Self::Reset => translations::COMMANDS_TITLE_RESET_SINGLE
154                    .message([name])
155                    .component(),
156                Self::Show(TitleKind::Title) => translations::COMMANDS_TITLE_SHOW_TITLE_SINGLE
157                    .message([name])
158                    .component(),
159                Self::Show(TitleKind::Subtitle) => {
160                    translations::COMMANDS_TITLE_SHOW_SUBTITLE_SINGLE
161                        .message([name])
162                        .component()
163                }
164                Self::Show(TitleKind::Actionbar) => {
165                    translations::COMMANDS_TITLE_SHOW_ACTIONBAR_SINGLE
166                        .message([name])
167                        .component()
168                }
169                Self::Times => translations::COMMANDS_TITLE_TIMES_SINGLE
170                    .message([name])
171                    .component(),
172            };
173        }
174
175        let count = TextComponent::plain(targets.len().to_string());
176        match self {
177            Self::Clear => translations::COMMANDS_TITLE_CLEARED_MULTIPLE
178                .message([count])
179                .component(),
180            Self::Reset => translations::COMMANDS_TITLE_RESET_MULTIPLE
181                .message([count])
182                .component(),
183            Self::Show(TitleKind::Title) => translations::COMMANDS_TITLE_SHOW_TITLE_MULTIPLE
184                .message([count])
185                .component(),
186            Self::Show(TitleKind::Subtitle) => translations::COMMANDS_TITLE_SHOW_SUBTITLE_MULTIPLE
187                .message([count])
188                .component(),
189            Self::Show(TitleKind::Actionbar) => {
190                translations::COMMANDS_TITLE_SHOW_ACTIONBAR_MULTIPLE
191                    .message([count])
192                    .component()
193            }
194            Self::Times => translations::COMMANDS_TITLE_TIMES_MULTIPLE
195                .message([count])
196                .component(),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use steel_registry::init_vanilla_registry;
204
205    use super::super::create_dispatcher;
206    use crate::command::brigadier::{CommandDispatcher, NodeId};
207    use crate::command::execution::{CommandSource, SteelArgumentType, SteelCommandRuntime};
208
209    fn child(
210        dispatcher: &CommandDispatcher<CommandSource, SteelCommandRuntime>,
211        parent: NodeId,
212        name: &str,
213    ) -> NodeId {
214        let Some(child) = dispatcher.children(parent).and_then(|children| {
215            children.iter().copied().find(|child| {
216                dispatcher
217                    .node(*child)
218                    .is_some_and(|node| node.name() == name)
219            })
220        }) else {
221            panic!("expected {name:?} command child");
222        };
223        child
224    }
225
226    #[test]
227    fn title_graph_matches_vanilla_shape() {
228        init_vanilla_registry();
229        let Ok(dispatcher) = create_dispatcher() else {
230            panic!("built-in commands should register");
231        };
232        let title = child(&dispatcher, dispatcher.root(), "title");
233        let Some(title_node) = dispatcher.node(title) else {
234            panic!("title root should exist");
235        };
236        assert!(title_node.is_restricted());
237        assert!(!title_node.is_executable());
238
239        let targets = dispatcher
240            .children(title)
241            .and_then(|children| children.first())
242            .copied();
243        let Some(targets) = targets else {
244            panic!("title targets should exist");
245        };
246        assert_eq!(
247            dispatcher
248                .node(targets)
249                .and_then(|node| node.argument_type()),
250            Some(&SteelArgumentType::players())
251        );
252
253        let Some(branches) = dispatcher.children(targets) else {
254            panic!("title branches should exist");
255        };
256        let branch_names = branches
257            .iter()
258            .map(|branch| {
259                let Some(node) = dispatcher.node(*branch) else {
260                    panic!("title branch should exist");
261                };
262                node.name()
263            })
264            .collect::<Vec<_>>();
265        assert_eq!(
266            branch_names,
267            ["clear", "reset", "title", "subtitle", "actionbar", "times"]
268        );
269
270        for branch in ["clear", "reset"] {
271            let branch = child(&dispatcher, targets, branch);
272            let Some(branch_node) = dispatcher.node(branch) else {
273                panic!("title branch should exist");
274            };
275            assert!(branch_node.is_executable());
276            assert!(dispatcher.children(branch).is_some_and(<[_]>::is_empty));
277        }
278
279        for branch in ["title", "subtitle", "actionbar"] {
280            let branch = child(&dispatcher, targets, branch);
281            let title_argument = child(&dispatcher, branch, "title");
282            let Some(title_argument_node) = dispatcher.node(title_argument) else {
283                panic!("title component argument should exist");
284            };
285            assert_eq!(
286                title_argument_node.argument_type(),
287                Some(&SteelArgumentType::component())
288            );
289            assert!(title_argument_node.is_executable());
290        }
291
292        let times = child(&dispatcher, targets, "times");
293        let fade_in = child(&dispatcher, times, "fadeIn");
294        assert_eq!(
295            dispatcher
296                .node(fade_in)
297                .and_then(|node| node.argument_type()),
298            Some(&SteelArgumentType::time(0))
299        );
300        let stay = child(&dispatcher, fade_in, "stay");
301        assert_eq!(
302            dispatcher.node(stay).and_then(|node| node.argument_type()),
303            Some(&SteelArgumentType::time(0))
304        );
305        let fade_out = child(&dispatcher, stay, "fadeOut");
306        let Some(fade_out_node) = dispatcher.node(fade_out) else {
307            panic!("fadeOut argument should exist");
308        };
309        assert_eq!(
310            fade_out_node.argument_type(),
311            Some(&SteelArgumentType::time(0))
312        );
313        assert!(fade_out_node.is_executable());
314    }
315}