steel_core/command/builtins/
title.rs1use 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}