Skip to main content

steel_core/command/builtins/
give.rs

1//! Vanilla item-giving command.
2
3use steel_protocol::packets::game::SoundSource;
4use steel_registry::{
5    data_components::vanilla_components::{CUSTOM_NAME, ITEM_NAME},
6    item_stack::ItemStack,
7    sound_events,
8};
9use steel_utils::{Identifier, translations};
10use text_components::TextComponent;
11
12use super::super::{
13    brigadier::{ArgumentType, CommandNodeBuilder, CommandSyntaxError},
14    execution::{
15        CommandSource, SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument,
16        literal,
17    },
18    registration::CommandRegistration,
19};
20use crate::{entity::Entity as _, inventory::container::Container as _, player::Player};
21
22const MAX_ALLOWED_ITEM_STACKS: i32 = 100;
23
24pub(super) fn registration() -> CommandRegistration<CommandSource> {
25    CommandRegistration::new(Identifier::vanilla_static("give"), |_| command())
26}
27
28fn command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
29    literal("give").then(
30        argument("targets", SteelArgumentType::players()).then(
31            argument("item", SteelArgumentType::item_stack())
32                .executes(give_default_count)
33                .then(
34                    argument("count", ArgumentType::integer(1, i32::MAX)).executes(give_with_count),
35                ),
36        ),
37    )
38}
39
40fn give_default_count(
41    context: &SteelCommandContext<CommandSource>,
42) -> Result<i32, CommandSyntaxError> {
43    give(context, 1)
44}
45
46fn give_with_count(
47    context: &SteelCommandContext<CommandSource>,
48) -> Result<i32, CommandSyntaxError> {
49    let Some(count) = context.integer("count") else {
50        return Err(missing_argument("count"));
51    };
52    give(context, count)
53}
54
55fn give(
56    context: &SteelCommandContext<CommandSource>,
57    count: i32,
58) -> Result<i32, CommandSyntaxError> {
59    let targets = context.players("targets")?;
60    let Some(prototype) = context.item_stack("item") else {
61        return Err(missing_argument("item"));
62    };
63    let max_allowed_count = prototype.max_stack_size() * MAX_ALLOWED_ITEM_STACKS;
64    if count > max_allowed_count {
65        let message = translations::COMMANDS_GIVE_FAILED_TOOMANYITEMS
66            .message([
67                TextComponent::from(max_allowed_count.to_string()),
68                item_display_name(prototype),
69            ])
70            .component();
71        context.source().send_failure(message);
72        return Ok(0);
73    }
74
75    for target in &targets {
76        give_to_player(target, prototype, count);
77    }
78
79    let message = if let [target] = targets.as_slice() {
80        translations::COMMANDS_GIVE_SUCCESS_SINGLE
81            .message([
82                TextComponent::from(count.to_string()),
83                item_display_name(prototype),
84                TextComponent::plain(target.plain_text_name()),
85            ])
86            .component()
87    } else {
88        translations::COMMANDS_GIVE_SUCCESS_MULTIPLE
89            .message([
90                TextComponent::from(count.to_string()),
91                item_display_name(prototype),
92                TextComponent::from(targets.len().to_string()),
93            ])
94            .component()
95    };
96    context.source().send_success(&message, true);
97
98    i32::try_from(targets.len()).map_err(|_| {
99        CommandSyntaxError::dynamic("Target player count exceeds the command result range")
100    })
101}
102
103fn give_to_player(player: &Player, prototype: &ItemStack, count: i32) {
104    let max_stack_size = prototype.max_stack_size();
105    let mut remaining = count;
106    while remaining > 0 {
107        let size = max_stack_size.min(remaining);
108        remaining -= size;
109        let mut stack = prototype.copy_with_count(size);
110        let added = player.inventory.lock().add(&mut stack);
111
112        if added && stack.is_empty() {
113            if let Some(item) = player.drop_item(prototype.copy_with_count(1), false, false) {
114                item.make_fake_item();
115            }
116            play_pickup_sound(player);
117            player.broadcast_inventory_changes();
118        } else if let Some(item) = player.drop_item(stack, false, false) {
119            item.set_no_pickup_delay();
120            item.set_owner(Some(player.gameprofile.id));
121        }
122    }
123}
124
125fn play_pickup_sound(player: &Player) {
126    let pitch = ((rand::random::<f32>() - rand::random::<f32>()) * 0.7 + 1.0) * 2.0;
127    player.get_world().play_sound_at(
128        &sound_events::ENTITY_ITEM_PICKUP,
129        SoundSource::Players,
130        player.position(),
131        0.2,
132        pitch,
133        None,
134    );
135}
136
137fn item_display_name(stack: &ItemStack) -> TextComponent {
138    stack
139        .get(CUSTOM_NAME)
140        .or_else(|| stack.get(ITEM_NAME))
141        .cloned()
142        .unwrap_or_else(|| TextComponent::plain(stack.item().key.to_string()))
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 give_graph_uses_players_item_stack_and_positive_count() {
177        init_vanilla_registry();
178        let Ok(dispatcher) = create_dispatcher() else {
179            panic!("built-in commands should register");
180        };
181        let give = child(&dispatcher, dispatcher.root(), "give");
182        let Some(give_node) = dispatcher.node(give) else {
183            panic!("give node should exist");
184        };
185        assert!(give_node.is_restricted());
186
187        let targets = child(&dispatcher, give, "targets");
188        assert_eq!(
189            dispatcher
190                .node(targets)
191                .and_then(|node| node.argument_type()),
192            Some(&SteelArgumentType::players())
193        );
194
195        let item = child(&dispatcher, targets, "item");
196        let Some(item_node) = dispatcher.node(item) else {
197            panic!("item node should exist");
198        };
199        assert_eq!(
200            item_node.argument_type(),
201            Some(&SteelArgumentType::item_stack())
202        );
203        assert!(item_node.is_executable());
204
205        let count = child(&dispatcher, item, "count");
206        let Some(count_node) = dispatcher.node(count) else {
207            panic!("count node should exist");
208        };
209        assert_eq!(
210            count_node.argument_type(),
211            Some(&SteelArgumentType::from(ArgumentType::integer(1, i32::MAX)))
212        );
213        assert!(count_node.is_executable());
214    }
215}