Skip to main content

steel_core/command/builtins/
give.rs

1//! Vanilla item-giving command.
2
3use steel_registry::{
4    data_components::vanilla_components::{CUSTOM_NAME, ITEM_NAME},
5    item_stack::ItemStack,
6};
7use steel_utils::{Identifier, translations};
8use text_components::TextComponent;
9
10use super::super::{
11    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
12    execution::{
13        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
14        literal,
15    },
16    registration::CommandRegistration,
17};
18use crate::{entity::Entity as _, player::Player};
19
20const MAX_ALLOWED_ITEM_STACKS: i32 = 100;
21
22pub(super) fn registration() -> CommandRegistration<CommandSource> {
23    CommandRegistration::new(Identifier::vanilla_static("give"), |_| command())
24}
25
26fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
27    literal("give").then(
28        argument("targets", SteelArgumentType::players()).then(
29            argument("item", SteelArgumentType::item_stack())
30                .executes(give_default_count)
31                .then(
32                    argument("count", ArgumentType::integer(1, i32::MAX)).executes(give_with_count),
33                ),
34        ),
35    )
36}
37
38fn give_default_count(
39    context: &SteelCommandContext<CommandSource>,
40) -> Result<i32, CommandSyntaxError> {
41    give(context, 1)
42}
43
44fn give_with_count(
45    context: &SteelCommandContext<CommandSource>,
46) -> Result<i32, CommandSyntaxError> {
47    let count = context.integer("count")?;
48    give(context, count)
49}
50
51fn give(
52    context: &SteelCommandContext<CommandSource>,
53    count: i32,
54) -> Result<i32, CommandSyntaxError> {
55    let targets = context.players("targets")?;
56    let prototype = context.item_stack("item")?;
57    let max_allowed_count = prototype.max_stack_size() * MAX_ALLOWED_ITEM_STACKS;
58    if count > max_allowed_count {
59        let message = translations::COMMANDS_GIVE_FAILED_TOOMANYITEMS
60            .message([
61                TextComponent::from(max_allowed_count.to_string()),
62                item_display_name(prototype),
63            ])
64            .component();
65        context.source().send_failure(message);
66        return Ok(0);
67    }
68
69    for target in &targets {
70        give_to_player(target, prototype, count);
71    }
72
73    let message = if let [target] = targets.as_slice() {
74        translations::COMMANDS_GIVE_SUCCESS_SINGLE
75            .message([
76                TextComponent::from(count.to_string()),
77                item_display_name(prototype),
78                TextComponent::plain(target.plain_text_name()),
79            ])
80            .component()
81    } else {
82        translations::COMMANDS_GIVE_SUCCESS_MULTIPLE
83            .message([
84                TextComponent::from(count.to_string()),
85                item_display_name(prototype),
86                TextComponent::from(targets.len().to_string()),
87            ])
88            .component()
89    };
90    context.source().send_success(&message, true);
91
92    i32::try_from(targets.len()).map_err(|_| {
93        CommandSyntaxError::dynamic("Target player count exceeds the command result range")
94    })
95}
96
97fn give_to_player(player: &Player, prototype: &ItemStack, count: i32) {
98    let max_stack_size = prototype.max_stack_size();
99    let mut remaining = count;
100    while remaining > 0 {
101        let size = max_stack_size.min(remaining);
102        remaining -= size;
103        let mut stack = prototype.copy_with_count(size);
104        if player.add_item_with_sound(&mut stack) {
105            if let Some(item) =
106                player.spawn_dropped_item(prototype.copy_with_count(1), false, false)
107            {
108                item.make_fake_item();
109            }
110            player.broadcast_inventory_changes();
111        } else if let Some(item) = player.spawn_dropped_item(stack, false, false) {
112            item.set_no_pickup_delay();
113            item.set_owner(Some(player.gameprofile.id()));
114        }
115    }
116}
117
118fn item_display_name(stack: &ItemStack) -> TextComponent {
119    stack
120        .get(CUSTOM_NAME)
121        .or_else(|| stack.get(ITEM_NAME))
122        .cloned()
123        .unwrap_or_else(|| TextComponent::plain(stack.item().key.to_string()))
124}
125
126#[cfg(test)]
127mod tests {
128    use steel_registry::init_vanilla_registry;
129
130    use super::super::create_dispatcher;
131    use super::*;
132    use crate::command::brigadier::{CommandDispatcher, NodeId};
133
134    type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
135
136    fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
137        let Some(children) = dispatcher.children(parent) else {
138            panic!("parent node should exist");
139        };
140        let Some(child) = children.iter().copied().find(|child| {
141            dispatcher
142                .node(*child)
143                .is_some_and(|node| node.name() == name)
144        }) else {
145            panic!("child {name} should exist");
146        };
147        child
148    }
149
150    #[test]
151    fn give_graph_uses_players_item_stack_and_positive_count() {
152        init_vanilla_registry();
153        let Ok(dispatcher) = create_dispatcher() else {
154            panic!("built-in commands should register");
155        };
156        let give = child(&dispatcher, dispatcher.root(), "give");
157        let Some(give_node) = dispatcher.node(give) else {
158            panic!("give node should exist");
159        };
160        assert!(give_node.is_restricted());
161
162        let targets = child(&dispatcher, give, "targets");
163        assert_eq!(
164            dispatcher
165                .node(targets)
166                .and_then(|node| node.argument_type()),
167            Some(&SteelArgumentType::players())
168        );
169
170        let item = child(&dispatcher, targets, "item");
171        let Some(item_node) = dispatcher.node(item) else {
172            panic!("item node should exist");
173        };
174        assert_eq!(
175            item_node.argument_type(),
176            Some(&SteelArgumentType::item_stack())
177        );
178        assert!(item_node.is_executable());
179
180        let count = child(&dispatcher, item, "count");
181        let Some(count_node) = dispatcher.node(count) else {
182            panic!("count node should exist");
183        };
184        assert_eq!(
185            count_node.argument_type(),
186            Some(&SteelArgumentType::from(ArgumentType::integer(1, i32::MAX)))
187        );
188        assert!(count_node.is_executable());
189    }
190}