steel_core/command/builtins/
give.rs1use 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 count = context.integer("count")?;
50 give(context, count)
51}
52
53fn give(
54 context: &SteelCommandContext<CommandSource>,
55 count: i32,
56) -> Result<i32, CommandSyntaxError> {
57 let targets = context.players("targets")?;
58 let prototype = context.item_stack("item")?;
59 let max_allowed_count = prototype.max_stack_size() * MAX_ALLOWED_ITEM_STACKS;
60 if count > max_allowed_count {
61 let message = translations::COMMANDS_GIVE_FAILED_TOOMANYITEMS
62 .message([
63 TextComponent::from(max_allowed_count.to_string()),
64 item_display_name(prototype),
65 ])
66 .component();
67 context.source().send_failure(message);
68 return Ok(0);
69 }
70
71 for target in &targets {
72 give_to_player(target, prototype, count);
73 }
74
75 let message = if let [target] = targets.as_slice() {
76 translations::COMMANDS_GIVE_SUCCESS_SINGLE
77 .message([
78 TextComponent::from(count.to_string()),
79 item_display_name(prototype),
80 TextComponent::plain(target.plain_text_name()),
81 ])
82 .component()
83 } else {
84 translations::COMMANDS_GIVE_SUCCESS_MULTIPLE
85 .message([
86 TextComponent::from(count.to_string()),
87 item_display_name(prototype),
88 TextComponent::from(targets.len().to_string()),
89 ])
90 .component()
91 };
92 context.source().send_success(&message, true);
93
94 i32::try_from(targets.len()).map_err(|_| {
95 CommandSyntaxError::dynamic("Target player count exceeds the command result range")
96 })
97}
98
99fn give_to_player(player: &Player, prototype: &ItemStack, count: i32) {
100 let max_stack_size = prototype.max_stack_size();
101 let mut remaining = count;
102 while remaining > 0 {
103 let size = max_stack_size.min(remaining);
104 remaining -= size;
105 let mut stack = prototype.copy_with_count(size);
106 let added = player.inventory.lock().add(&mut stack);
107
108 if added && stack.is_empty() {
109 if let Some(item) = player.drop_item(prototype.copy_with_count(1), false, false) {
110 item.make_fake_item();
111 }
112 play_pickup_sound(player);
113 player.broadcast_inventory_changes();
114 } else if let Some(item) = player.drop_item(stack, false, false) {
115 item.set_no_pickup_delay();
116 item.set_owner(Some(player.gameprofile.id));
117 }
118 }
119}
120
121fn play_pickup_sound(player: &Player) {
122 let pitch = ((rand::random::<f32>() - rand::random::<f32>()) * 0.7 + 1.0) * 2.0;
123 player.get_world().play_sound_at(
124 &sound_events::ENTITY_ITEM_PICKUP,
125 SoundSource::Players,
126 player.position(),
127 0.2,
128 pitch,
129 None,
130 );
131}
132
133fn item_display_name(stack: &ItemStack) -> TextComponent {
134 stack
135 .get(CUSTOM_NAME)
136 .or_else(|| stack.get(ITEM_NAME))
137 .cloned()
138 .unwrap_or_else(|| TextComponent::plain(stack.item().key.to_string()))
139}
140
141#[cfg(test)]
142mod tests {
143 use steel_registry::init_vanilla_registry;
144
145 use super::super::create_dispatcher;
146 use super::*;
147 use crate::command::brigadier::{CommandDispatcher, NodeId};
148
149 type Dispatcher = CommandDispatcher<CommandSource, SteelCommandRuntime>;
150
151 fn child(dispatcher: &Dispatcher, parent: NodeId, name: &str) -> NodeId {
152 let Some(children) = dispatcher.children(parent) else {
153 panic!("parent node should exist");
154 };
155 let Some(child) = children.iter().copied().find(|child| {
156 dispatcher
157 .node(*child)
158 .is_some_and(|node| node.name() == name)
159 }) else {
160 panic!("child {name} should exist");
161 };
162 child
163 }
164
165 #[test]
166 fn give_graph_uses_players_item_stack_and_positive_count() {
167 init_vanilla_registry();
168 let Ok(dispatcher) = create_dispatcher() else {
169 panic!("built-in commands should register");
170 };
171 let give = child(&dispatcher, dispatcher.root(), "give");
172 let Some(give_node) = dispatcher.node(give) else {
173 panic!("give node should exist");
174 };
175 assert!(give_node.is_restricted());
176
177 let targets = child(&dispatcher, give, "targets");
178 assert_eq!(
179 dispatcher
180 .node(targets)
181 .and_then(|node| node.argument_type()),
182 Some(&SteelArgumentType::players())
183 );
184
185 let item = child(&dispatcher, targets, "item");
186 let Some(item_node) = dispatcher.node(item) else {
187 panic!("item node should exist");
188 };
189 assert_eq!(
190 item_node.argument_type(),
191 Some(&SteelArgumentType::item_stack())
192 );
193 assert!(item_node.is_executable());
194
195 let count = child(&dispatcher, item, "count");
196 let Some(count_node) = dispatcher.node(count) else {
197 panic!("count node should exist");
198 };
199 assert_eq!(
200 count_node.argument_type(),
201 Some(&SteelArgumentType::from(ArgumentType::integer(1, i32::MAX)))
202 );
203 assert!(count_node.is_executable());
204 }
205}