steel_core/inventory/menu/kinds/
grindstone_menu.rs1use std::sync::Arc;
3
4use steel_registry::data_components::vanilla_components::MAX_DAMAGE;
5use steel_registry::vanilla_blocks;
6use steel_registry::{
7 REGISTRY, RegistryExt, TaggedRegistryExt,
8 blocks::block_state_ext::BlockStateExt,
9 data_components::{
10 components::ItemEnchantments,
11 vanilla_components::{ENCHANTMENTS, REPAIR_COST, STORED_ENCHANTMENTS},
12 },
13 item_stack::ItemStack,
14 vanilla_enchantment_tags::EnchantmentTag,
15 vanilla_items, vanilla_menu_types,
16};
17use steel_utils::{
18 BlockPos,
19 locks::{IntoShared, Shared},
20};
21
22use crate::{
23 inventory::{
24 container::{ResultContainer, SimpleContainer},
25 menu::kinds::AnvilKind,
26 prelude::*,
27 slots::GrindstoneResultHandler,
28 },
29 player::player_inventory::PlayerInventory,
30 world::World,
31};
32
33use std::cmp::max;
34
35#[must_use]
37pub fn grindstone(
38 inventory: Shared<PlayerInventory>,
39 container_id: u8,
40 pos: BlockPos,
41 world: &Arc<World>,
42) -> Menu {
43 let input_container = SimpleContainer::new(2).into_shared();
44
45 let result_container = ResultContainer::new().into_shared();
46
47 let mut builder = MenuBuilder::new(&vanilla_menu_types::GRINDSTONE, container_id);
48
49 let input = builder.section_all_with(
50 &input_container,
51 SectionKind::restricted(|_index, item_stack| {
52 item_stack.is_damageable_item()
53 || item_stack
54 .get_enchantments_for_crafting()
55 .is_some_and(|e| !e.is_empty())
56 }),
57 );
58 let result = builder.result_slot(GrindstoneResultHandler::new(
59 input_container.clone(),
60 result_container.clone(),
61 pos,
62 world.clone(),
63 ));
64
65 let player = builder.player_inventory(&inventory);
66
67 builder.route_with_remainder_policy(
68 result,
69 player.all(),
70 FillDirection::Backward,
71 FakeResultRemainderPolicy::Discard,
72 );
73 builder.route(input, player.all(), FillDirection::Forward);
74 builder.drain(input);
75
76 builder.build(GrindstoneKind {
77 input_container,
78 result_container,
79 block_pos: pos,
80 world: Arc::clone(world),
81 input,
82 player,
83 })
84}
85
86pub struct GrindstoneKind {
88 input_container: Shared<SimpleContainer>,
90 result_container: Shared<ResultContainer>,
92 block_pos: BlockPos,
93 world: Arc<World>,
94 input: Section,
95 player: PlayerInventorySections,
96}
97
98unsafe impl steel_utils::DowncastType for GrindstoneKind {
101 const TYPE_KEY: steel_utils::DowncastTypeKey =
102 steel_utils::DowncastTypeKey::new("steel:menu/grindstone");
103}
104
105impl GrindstoneKind {
106 #[tracing::instrument(skip(self, _behavior, player, guard), level = "info", fields(player = %player.gameprofile.name))]
111 pub(crate) fn create_result(
112 &mut self,
113 _behavior: &mut MenuBehavior,
114 guard: &mut ContainerLockGuard,
115 player: &Player,
116 ) {
117 let Some([input_container, result_container]) = guard.get_disjoint_mut([
118 ContainerId::from_arc(&self.input_container),
119 ContainerId::from_arc(&self.result_container),
120 ]) else {
121 log::warn!("failed to lock input and/or result containers to create grindstone result");
122 return;
123 };
124
125 let [first, second] = input_container.items() else {
126 log::warn!("input_container in grindstone menu does not fit expected shape");
127 return;
128 };
129
130 if first.is_empty() && second.is_empty() {
131 result_container.set_item(0, ItemStack::empty());
132 return;
133 }
134
135 if first.count() <= 1 && second.count() <= 1 {
136 if !first.is_empty() && !second.is_empty() {
137 result_container.set_item(
138 0,
139 GrindstoneKind::merge_items(first.clone(), second.clone()),
140 );
141 } else {
142 let item = if first.is_empty() { second } else { first };
143
144 if item
145 .get_enchantments_for_crafting()
146 .is_some_and(|e| !e.is_empty())
147 {
148 result_container
149 .set_item(0, GrindstoneKind::remove_non_curses_from(item.clone()));
150 } else {
151 result_container.set_item(0, ItemStack::empty());
152 }
153 }
154 } else {
155 result_container.set_item(0, ItemStack::empty());
156 }
157 }
158
159 #[must_use]
161 fn merge_items(first: ItemStack, second: ItemStack) -> ItemStack {
162 if !first.is(second.item()) {
163 return ItemStack::empty();
164 }
165
166 let durability = max(first.get_max_damage(), second.get_max_damage());
167 let remaining1 = first.get_max_damage() - first.get_damage_value();
168 let remaining2 = second.get_max_damage() - second.get_damage_value();
169 let remaining = remaining1 + remaining2 + durability * 5 / 100;
170 let mut count = 1;
171
172 if !first.is_damageable_item() {
173 count = 2;
174
175 if first.max_stack_size() < 2 || !ItemStack::matches(&first, &second) {
176 return ItemStack::empty();
177 }
178 }
179
180 let mut new_item = first.copy_with_count(count);
181 if new_item.is_damageable_item() {
182 new_item.set(MAX_DAMAGE, durability);
183 new_item.set_damage_value(max(durability - remaining, 0));
184 }
185
186 GrindstoneKind::merge_enchantments_from(&mut new_item, &second);
187 GrindstoneKind::remove_non_curses_from(new_item)
188 }
189
190 fn merge_enchantments_from(target: &mut ItemStack, source: &ItemStack) {
194 let Some(source_enchantments) = source.get_enchantments_for_crafting() else {
195 return;
196 };
197
198 for (id, level) in source_enchantments.iter() {
199 let is_curse = REGISTRY.enchantments.by_key(id).is_some_and(|enchantment| {
200 REGISTRY
201 .enchantments
202 .is_in_tag(enchantment, &EnchantmentTag::CURSE)
203 });
204
205 let target_level = target
206 .get_enchantments_for_crafting()
207 .map_or(0, |enchantments| enchantments.get_level(id));
208
209 if !is_curse || target_level == 0 {
210 target.upgrade_enchantment(id.clone(), *level);
211 }
212 }
213 }
214
215 #[must_use]
217 pub fn remove_non_curses_from(mut item: ItemStack) -> ItemStack {
218 let Some(enchantments) = item.get_enchantments_for_crafting() else {
219 return ItemStack::empty();
220 };
221
222 let mut new_enchantments = ItemEnchantments::empty();
223
224 enchantments
225 .iter()
226 .filter(|(id, _)| {
227 REGISTRY.enchantments.by_key(id).is_some_and(|enchantment| {
228 REGISTRY
229 .enchantments
230 .is_in_tag(enchantment, &EnchantmentTag::CURSE)
231 })
232 })
233 .for_each(|(id, level)| new_enchantments.set(id.clone(), *level));
234
235 let mut repair_cost = 0;
236 for _ in 0..new_enchantments.len() {
237 repair_cost = AnvilKind::calculate_increased_repair_cost(repair_cost);
238 }
239
240 item.set(REPAIR_COST, repair_cost);
241 if item.is(&vanilla_items::ENCHANTED_BOOK) {
242 if new_enchantments.is_empty() {
243 item.set(STORED_ENCHANTMENTS, new_enchantments);
245 item.set_item(&vanilla_items::BOOK.key);
246 return item;
247 }
248 item.set(STORED_ENCHANTMENTS, new_enchantments);
249 } else {
250 item.set(ENCHANTMENTS, new_enchantments);
251 }
252
253 item
254 }
255}
256
257impl MenuKind for GrindstoneKind {
258 fn still_valid(&self, _behavior: &MenuBehavior, player: &Player) -> bool {
260 let state = self.world.get_block_state(self.block_pos);
261 state.get_block() == &vanilla_blocks::GRINDSTONE
262 && player.is_within_block_interaction_range_with_buffer(self.block_pos, 4.0)
263 }
264
265 fn slots_changed(
266 &mut self,
267 behavior: &mut MenuBehavior,
268 guard: &mut ContainerLockGuard,
269 player: &Player,
270 ) {
271 self.create_result(behavior, guard, player);
272 }
273
274 fn removed(&mut self, _behavior: &mut MenuBehavior, _player: &Player) {
276 self.result_container.lock().set_item(0, ItemStack::empty());
277 }
278
279 fn quick_move(
280 &mut self,
281 behavior: &mut MenuBehavior,
282 guard: &mut ContainerLockGuard,
283 slot_index: usize,
284 player: &Player,
285 ) -> Option<ItemStack> {
286 if !self.player.all().contains(slot_index) {
287 return None;
288 }
289
290 let clicked = behavior.slots()[slot_index].get_item(guard).clone();
291 if clicked.is_empty() {
292 return Some(ItemStack::empty());
293 }
294
295 let Some(inputs) = guard.get(ContainerId::from_arc(&self.input_container)) else {
296 log::warn!("input container not locked while quick-moving in the grindstone menu");
297 return Some(ItemStack::empty());
298 };
299 let both_inputs_filled = !inputs.get_item(0).is_empty() && !inputs.get_item(1).is_empty();
300
301 let mut remaining = clicked.clone();
302 let destination = if !both_inputs_filled {
303 self.input
304 } else if self.player.main().contains(slot_index) {
305 self.player.hotbar()
306 } else {
307 self.player.main()
308 };
309
310 let moved = behavior.move_item_stack_to(
311 guard,
312 slot_index,
313 &mut remaining,
314 destination.start(),
315 destination.end(),
316 FillDirection::Forward,
317 );
318 if !moved {
319 return Some(ItemStack::empty());
320 }
321
322 behavior.update_quick_move_source(guard, slot_index, &remaining, &clicked);
323 if remaining.count() == clicked.count() {
324 return Some(ItemStack::empty());
325 }
326 if let Some(remainder) = behavior.slots()[slot_index].on_take(guard, &remaining, player) {
327 player.add_item_or_drop_with_guard(guard, remainder);
328 }
329 Some(clicked)
330 }
331}