steel_core/command/builtins/
clear.rs1use std::{slice, sync::Arc};
4
5use steel_registry::item_stack::ItemStack;
6use steel_utils::{Identifier, translations};
7use text_components::TextComponent;
8
9use super::super::{
10 brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
11 execution::{
12 CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
13 literal,
14 },
15 registration::CommandRegistration,
16};
17use crate::{entity::Entity as _, player::Player};
18
19pub(super) fn registration() -> CommandRegistration<CommandSource> {
20 CommandRegistration::new(Identifier::vanilla_static("clear"), |_| command())
21}
22
23fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
24 literal("clear").executes(clear_self).then(
25 argument("targets", SteelArgumentType::players())
26 .executes(clear_targets)
27 .then(
28 argument("item", SteelArgumentType::item_predicate())
29 .executes(clear_matching)
30 .then(
31 argument("maxCount", ArgumentType::integer(0, i32::MAX))
32 .executes(clear_matching_with_limit),
33 ),
34 ),
35 )
36}
37
38fn clear_self(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
39 let Some(player) = context.source().player() else {
40 return Err(CommandSyntaxError::dynamic(TextComponent::from(
41 &translations::PERMISSIONS_REQUIRES_PLAYER,
42 )));
43 };
44 clear_players(context, slice::from_ref(player), &matches_any_item, -1)
45}
46
47fn clear_targets(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
48 let targets = context.players("targets")?;
49 clear_players(context, &targets, &matches_any_item, -1)
50}
51
52fn clear_matching(context: &SteelCommandContext<CommandSource>) -> Result<i32, CommandSyntaxError> {
53 clear_matching_with_count(context, -1)
54}
55
56fn clear_matching_with_limit(
57 context: &SteelCommandContext<CommandSource>,
58) -> Result<i32, CommandSyntaxError> {
59 let Some(max_count) = context.integer("maxCount") else {
60 return Err(missing_argument("maxCount"));
61 };
62 clear_matching_with_count(context, max_count)
63}
64
65fn clear_matching_with_count(
66 context: &SteelCommandContext<CommandSource>,
67 max_count: i32,
68) -> Result<i32, CommandSyntaxError> {
69 let targets = context.players("targets")?;
70 let Some(predicate) = context.item_predicate("item") else {
71 return Err(missing_argument("item"));
72 };
73 clear_players(
74 context,
75 &targets,
76 &|stack| predicate.matches(stack),
77 max_count,
78 )
79}
80
81fn clear_players(
82 context: &SteelCommandContext<CommandSource>,
83 targets: &[Arc<Player>],
84 predicate: &dyn Fn(&ItemStack) -> bool,
85 max_count: i32,
86) -> Result<i32, CommandSyntaxError> {
87 let mut count = 0;
88 for target in targets {
89 count += target.clear_or_count_matching_items(predicate, max_count);
90 }
91
92 if count == 0 {
93 let message = if let [target] = targets {
94 translations::CLEAR_FAILED_SINGLE
95 .message([TextComponent::plain(target.plain_text_name())])
96 .component()
97 } else {
98 translations::CLEAR_FAILED_MULTIPLE
99 .message([TextComponent::plain(targets.len().to_string())])
100 .component()
101 };
102 return Err(CommandSyntaxError::dynamic(message));
103 }
104
105 let count_component = TextComponent::plain(count.to_string());
106 let message = if max_count == 0 {
107 if let [target] = targets {
108 translations::COMMANDS_CLEAR_TEST_SINGLE
109 .message([
110 count_component,
111 TextComponent::plain(target.plain_text_name()),
112 ])
113 .component()
114 } else {
115 translations::COMMANDS_CLEAR_TEST_MULTIPLE
116 .message([
117 count_component,
118 TextComponent::plain(targets.len().to_string()),
119 ])
120 .component()
121 }
122 } else if let [target] = targets {
123 translations::COMMANDS_CLEAR_SUCCESS_SINGLE
124 .message([
125 count_component,
126 TextComponent::plain(target.plain_text_name()),
127 ])
128 .component()
129 } else {
130 translations::COMMANDS_CLEAR_SUCCESS_MULTIPLE
131 .message([
132 count_component,
133 TextComponent::plain(targets.len().to_string()),
134 ])
135 .component()
136 };
137 context.source().send_success(&message, true);
138 Ok(count)
139}
140
141const fn matches_any_item(_stack: &ItemStack) -> bool {
142 true
143}
144
145fn missing_argument(name: &str) -> CommandSyntaxError {
146 CommandSyntaxError::dynamic(format!(
147 "Parsed value for {name} is missing from the command context"
148 ))
149}
150
151#[cfg(test)]
152mod tests {
153 use steel_registry::init_vanilla_registry;
154
155 use super::super::create_dispatcher;
156 use super::*;
157 use crate::command::brigadier::{CommandDispatcher, NodeId};
158
159 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
160
161 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
162 let Some(children) = dispatcher.children(parent) else {
163 panic!("parent node should exist");
164 };
165 let Some(child) = children.iter().copied().find(|child| {
166 dispatcher
167 .node(*child)
168 .is_some_and(|node| node.name() == name)
169 }) else {
170 panic!("child {name} should exist");
171 };
172 child
173 }
174
175 #[test]
176 fn clear_graph_matches_vanilla_argument_shape() {
177 init_vanilla_registry();
178 let Ok(dispatcher) = create_dispatcher() else {
179 panic!("built-in commands should register");
180 };
181 let clear = child(&dispatcher, dispatcher.root(), "clear");
182 let Some(clear_node) = dispatcher.node(clear) else {
183 panic!("clear node should exist");
184 };
185 assert!(clear_node.is_restricted());
186 assert!(clear_node.is_executable());
187
188 let targets = child(&dispatcher, clear, "targets");
189 assert_eq!(
190 dispatcher
191 .node(targets)
192 .and_then(|node| node.argument_type()),
193 Some(&SteelArgumentType::players())
194 );
195 assert!(matches!(
196 dispatcher.node(targets),
197 Some(node) if node.is_executable()
198 ));
199
200 let item = child(&dispatcher, targets, "item");
201 assert_eq!(
202 dispatcher.node(item).and_then(|node| node.argument_type()),
203 Some(&SteelArgumentType::item_predicate())
204 );
205 assert!(matches!(
206 dispatcher.node(item),
207 Some(node) if node.is_executable()
208 ));
209
210 let max_count = child(&dispatcher, item, "maxCount");
211 assert_eq!(
212 dispatcher
213 .node(max_count)
214 .and_then(|node| node.argument_type()),
215 Some(&SteelArgumentType::from(ArgumentType::integer(0, i32::MAX)))
216 );
217 assert!(matches!(
218 dispatcher.node(max_count),
219 Some(node) if node.is_executable()
220 ));
221 }
222}