Skip to main content

steel_core/command/builtins/
clear.rs

1//! Vanilla player inventory clearing command.
2
3use 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}