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 max_count = context.integer("maxCount")?;
60    clear_matching_with_count(context, max_count)
61}
62
63fn clear_matching_with_count(
64    context: &SteelCommandContext<CommandSource>,
65    max_count: i32,
66) -> Result<i32, CommandSyntaxError> {
67    let targets = context.players("targets")?;
68    let predicate = context.item_predicate("item")?;
69
70    clear_players(
71        context,
72        &targets,
73        &|stack| predicate.matches(stack),
74        max_count,
75    )
76}
77
78fn clear_players(
79    context: &SteelCommandContext<CommandSource>,
80    targets: &[Arc<Player>],
81    predicate: &dyn Fn(&ItemStack) -> bool,
82    max_count: i32,
83) -> Result<i32, CommandSyntaxError> {
84    let mut count = 0;
85    for target in targets {
86        count += target.clear_or_count_matching_items(predicate, max_count);
87    }
88
89    if count == 0 {
90        let message = if let [target] = targets {
91            translations::CLEAR_FAILED_SINGLE
92                .message([TextComponent::plain(target.plain_text_name())])
93                .component()
94        } else {
95            translations::CLEAR_FAILED_MULTIPLE
96                .message([TextComponent::plain(targets.len().to_string())])
97                .component()
98        };
99        return Err(CommandSyntaxError::dynamic(message));
100    }
101
102    let count_component = TextComponent::plain(count.to_string());
103    let message = if max_count == 0 {
104        if let [target] = targets {
105            translations::COMMANDS_CLEAR_TEST_SINGLE
106                .message([
107                    count_component,
108                    TextComponent::plain(target.plain_text_name()),
109                ])
110                .component()
111        } else {
112            translations::COMMANDS_CLEAR_TEST_MULTIPLE
113                .message([
114                    count_component,
115                    TextComponent::plain(targets.len().to_string()),
116                ])
117                .component()
118        }
119    } else if let [target] = targets {
120        translations::COMMANDS_CLEAR_SUCCESS_SINGLE
121            .message([
122                count_component,
123                TextComponent::plain(target.plain_text_name()),
124            ])
125            .component()
126    } else {
127        translations::COMMANDS_CLEAR_SUCCESS_MULTIPLE
128            .message([
129                count_component,
130                TextComponent::plain(targets.len().to_string()),
131            ])
132            .component()
133    };
134    context.source().send_success(&message, true);
135    Ok(count)
136}
137
138const fn matches_any_item(_stack: &ItemStack) -> bool {
139    true
140}
141
142#[cfg(test)]
143mod tests {
144    use steel_registry::init_vanilla_registry;
145
146    use super::super::create_dispatcher;
147    use super::*;
148    use crate::command::brigadier::{CommandDispatcher, NodeId};
149
150    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
151
152    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
153        let Some(children) = dispatcher.children(parent) else {
154            panic!("parent node should exist");
155        };
156        let Some(child) = children.iter().copied().find(|child| {
157            dispatcher
158                .node(*child)
159                .is_some_and(|node| node.name() == name)
160        }) else {
161            panic!("child {name} should exist");
162        };
163        child
164    }
165
166    #[test]
167    fn clear_graph_matches_vanilla_argument_shape() {
168        init_vanilla_registry();
169        let Ok(dispatcher) = create_dispatcher() else {
170            panic!("built-in commands should register");
171        };
172        let clear = child(&dispatcher, dispatcher.root(), "clear");
173        let Some(clear_node) = dispatcher.node(clear) else {
174            panic!("clear node should exist");
175        };
176        assert!(clear_node.is_restricted());
177        assert!(clear_node.is_executable());
178
179        let targets = child(&dispatcher, clear, "targets");
180        assert_eq!(
181            dispatcher
182                .node(targets)
183                .and_then(|node| node.argument_type()),
184            Some(&SteelArgumentType::players())
185        );
186        assert!(matches!(
187            dispatcher.node(targets),
188            Some(node) if node.is_executable()
189        ));
190
191        let item = child(&dispatcher, targets, "item");
192        assert_eq!(
193            dispatcher.node(item).and_then(|node| node.argument_type()),
194            Some(&SteelArgumentType::item_predicate())
195        );
196        assert!(matches!(
197            dispatcher.node(item),
198            Some(node) if node.is_executable()
199        ));
200
201        let max_count = child(&dispatcher, item, "maxCount");
202        assert_eq!(
203            dispatcher
204                .node(max_count)
205                .and_then(|node| node.argument_type()),
206            Some(&SteelArgumentType::from(ArgumentType::integer(0, i32::MAX)))
207        );
208        assert!(matches!(
209            dispatcher.node(max_count),
210            Some(node) if node.is_executable()
211        ));
212    }
213}