use std::any::Any; use std::sync::Arc; use pumpkin_data::Enchantment; use pumpkin_data::data_component_impl::EnchantableImpl; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::screen::WindowType; use pumpkin_data::statistic::{CustomStatistic, StatisticCategory}; use pumpkin_data::tag::{Enchantment as EnchantmentTag, Taggable}; use pumpkin_util::random::{RandomImpl, legacy_rand::LegacyRand}; use pumpkin_world::inventory::Inventory; use crate::{ player::player_inventory::PlayerInventory, screen_handler::{ InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour, ScreenHandlerFuture, offer_or_drop_stack, }, slot::{BoxFuture, NormalSlot, Slot}, window_property::{EnchantmentTable, WindowProperty}, }; struct LapisSlot(NormalSlot); fn is_lapis(stack: &ItemStack) -> bool { stack.item == &Item::LAPIS_LAZULI } impl LapisSlot { fn new(inventory: Arc) -> Self { Self(NormalSlot::new(inventory, 1)) } } impl Slot for LapisSlot { fn get_inventory(&self) -> Arc { self.0.get_inventory() } fn get_index(&self) -> usize { self.0.get_index() } fn set_id(&self, id: usize) { self.0.set_id(id); } fn can_insert<'a>(&'a self, stack: &'a ItemStack) -> BoxFuture<'a, bool> { Box::pin(async move { is_lapis(stack) }) } fn mark_dirty(&self) -> BoxFuture<'_, ()> { self.0.mark_dirty() } } pub struct EnchantingTableScreenHandler { pub inventory: Arc, behaviour: ScreenHandlerBehaviour, pub level_requirements: [i32; 3], pub enchantment_id: [i32; 3], pub enchantment_level: [i32; 3], pub enchantment_seed: i32, pub bookshelf_count: i32, } impl EnchantingTableScreenHandler { pub fn new( sync_id: u8, player_inventory: &Arc, inventory: &Arc, enchantment_seed: i32, bookshelf_count: i32, ) -> Self { let mut handler = Self { inventory: inventory.clone(), behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Enchantment)), level_requirements: [0; 3], enchantment_id: [-1; 3], enchantment_level: [-1; 3], enchantment_seed, bookshelf_count, }; // Enchanting slots: 0 is item, 1 is lapis handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), 0))); handler.add_slot(Arc::new(LapisSlot::new(inventory.clone()))); let player_inventory: Arc = player_inventory.clone(); handler.add_player_slots(&player_inventory); handler } pub async fn update_enchantments(&mut self, _player: &dyn InventoryPlayer) { let item = self.inventory.get_stack(0).await; if item.is_empty() || item.has_enchantments() { for i in 0..3 { self.level_requirements[i] = 0; self.enchantment_id[i] = -1; self.enchantment_level[i] = -1; } } else { let enchantability = item .get_data_component::() .map_or(0, |e| e.value); if enchantability <= 0 { for i in 0..3 { self.level_requirements[i] = 0; self.enchantment_id[i] = -1; self.enchantment_level[i] = -1; } } else { let mut random = LegacyRand::from_seed(self.enchantment_seed as u64); for i in 0..3 { let level = self.calculate_level_requirement(&mut random, i, enchantability); self.level_requirements[i] = level; } for i in 0..3 { if self.level_requirements[i] > 0 { let mut random = self.create_enchantment_random(i); let enchantments = Self::get_enchantment_list( &mut random, &item, i, self.level_requirements[i], ); if enchantments.is_empty() { self.enchantment_id[i] = -1; self.enchantment_level[i] = -1; } else { let clue_index = random.next_bounded_i32(enchantments.len() as i32) as usize; let clue = enchantments[clue_index]; self.enchantment_id[i] = clue.0.id as i32; self.enchantment_level[i] = clue.1; } } else { self.enchantment_id[i] = -1; self.enchantment_level[i] = -1; } } } } self.send_property_updates().await; } fn calculate_level_requirement( &self, random: &mut LegacyRand, slot: usize, _enchantability: i32, ) -> i32 { let b = self.bookshelf_count; let level = random.next_bounded_i32(8) + 1 + (b >> 1) + random.next_bounded_i32(b + 1); match slot { 0 => (level / 3).max(1), 1 => (level * 2 / 3 + 7).max(1), 2 => level.max(b * 2).max(1), _ => 0, } } const fn create_enchantment_random(&self, slot: usize) -> LegacyRand { LegacyRand::from_seed(self.enchantment_seed.wrapping_add(slot as i32) as u64) } fn get_enchantment_list( random: &mut LegacyRand, item: &ItemStack, _slot: usize, level: i32, ) -> Vec<(&'static Enchantment, i32)> { let enchantability = item .get_data_component::() .map_or(0, |e| e.value); let mut enchant_level = level + 1 + random.next_bounded_i32(enchantability / 4 + 1) + random.next_bounded_i32(enchantability / 4 + 1); let bonus = (random.next_f32() + random.next_f32() - 1.0) * 0.15; enchant_level = (enchant_level as f32 * (1.0 + bonus)).round() as i32; enchant_level = enchant_level.max(1); let mut available = Vec::new(); for enchant in Enchantment::all() { if enchant.has_tag(&EnchantmentTag::MINECRAFT_IN_ENCHANTING_TABLE) && enchant.can_enchant(item.item) { for l in (1..=enchant.max_level).rev() { if enchant_level >= enchant.min_cost.calculate(l) && enchant_level <= enchant.max_cost.calculate(l) { available.push((*enchant, l)); break; } } } } if available.is_empty() { return Vec::new(); } let total_weight: i32 = available.iter().map(|(e, _)| e.weight).sum(); if total_weight <= 0 { return Vec::new(); } let mut weight = random.next_bounded_i32(total_weight); let mut selected = None; for (e, l) in &available { weight -= e.weight; if weight < 0 { selected = Some((*e, *l)); break; } } let mut result = Vec::new(); if let Some(s) = selected { result.push(s); // Add more? let mut current_level = enchant_level; while random.next_bounded_i32(50) <= (current_level + 1) / 2 { available.retain(|(e, _)| { for (se, _) in &result { if !e.are_compatible(se) { return false; } } true }); if available.is_empty() { break; } let total_weight: i32 = available.iter().map(|(e, _)| e.weight).sum(); let mut weight = random.next_bounded_i32(total_weight); for (e, l) in &available { weight -= e.weight; if weight < 0 { result.push((*e, *l)); break; } } current_level /= 2; } } result } async fn send_property_updates(&self) { if let Some(sync_handler) = self.behaviour.sync_handler.as_ref() { for i in 0..3 { let (id, val) = WindowProperty::new( EnchantmentTable::LevelRequirement { slot: i as u8 }, self.level_requirements[i] as i16, ) .into_tuple(); sync_handler .update_property(&self.behaviour, id as i32, val as i32) .await; let (id, val) = WindowProperty::new( EnchantmentTable::EnchantmentId { slot: i as u8 }, self.enchantment_id[i] as i16, ) .into_tuple(); sync_handler .update_property(&self.behaviour, id as i32, val as i32) .await; let (id, val) = WindowProperty::new( EnchantmentTable::EnchantmentLevel { slot: i as u8 }, self.enchantment_level[i] as i16, ) .into_tuple(); sync_handler .update_property(&self.behaviour, id as i32, val as i32) .await; } let (id, val) = WindowProperty::new( EnchantmentTable::EnchantmentSeed, (self.enchantment_seed & 0xFFFF) as i16, ) .into_tuple(); sync_handler .update_property(&self.behaviour, id as i32, val as i32) .await; } } } impl ScreenHandler for EnchantingTableScreenHandler { fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn get_behaviour(&self) -> &ScreenHandlerBehaviour { &self.behaviour } fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour { &mut self.behaviour } fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> { Box::pin(async move { self.default_on_closed(player).await; self.inventory.on_close().await; // Return items to player for i in 0..2 { let stack = self.inventory.remove_stack(i).await; if !stack.is_empty() { offer_or_drop_stack(player, stack).await; } } }) } fn on_button_click<'a>( &'a mut self, player: &'a dyn InventoryPlayer, id: i32, ) -> ScreenHandlerFuture<'a, bool> { Box::pin(async move { if !(0..3).contains(&id) { return false; } let level_req = self.level_requirements[id as usize]; if player.experience_level() < level_req && !player.is_creative() { return false; } let mut lapis_stack = self.inventory.get_stack(1).await; let lapis_cost = (id + 1) as u8; if !player.is_creative() && (lapis_stack.is_empty() || !is_lapis(&lapis_stack) || lapis_stack.item_count < lapis_cost) { return false; } // Perform enchantment let mut item_stack = self.inventory.get_stack(0).await; if item_stack.is_empty() || item_stack.has_enchantments() { return false; } let mut random = self.create_enchantment_random(id as usize); let enchantments = Self::get_enchantment_list(&mut random, &item_stack, id as usize, level_req); if enchantments.is_empty() { return false; } if !player.is_creative() { player.add_experience_levels(-(id + 1)).await; lapis_stack.decrement(lapis_cost); self.inventory.set_stack(1, lapis_stack).await; } for (enchant, level) in enchantments { item_stack.add_enchantment(enchant, level as u16); } self.inventory.set_stack(0, item_stack).await; // Update seed player.set_enchantment_seed(rand::random()).await; self.enchantment_seed = player.enchantment_seed(); self.update_enchantments(player).await; self.send_content_updates().await; player .increment_stat( StatisticCategory::Custom, CustomStatistic::EnchantItem as i32, 1, ) .await; true }) } fn quick_move<'a>( &'a mut self, player: &'a dyn InventoryPlayer, // FIX: Changed _player to player slot_index: i32, ) -> ItemStackFuture<'a> { Box::pin(async move { let mut stack_left = ItemStack::EMPTY.clone(); let slot = self.get_behaviour().slots[slot_index as usize].clone(); if slot.has_stack().await { let mut slot_stack = slot.get_stack().await; stack_left = slot_stack.clone(); if slot_index < 2 { // From enchanting to player if !self .insert_item( &mut slot_stack, 2, self.get_behaviour().slots.len() as i32, true, ) .await { return ItemStack::EMPTY.clone(); } } else { // From player to enchanting // Lapis check if slot_stack.item == &Item::LAPIS_LAZULI { if !self.insert_item(&mut slot_stack, 1, 2, false).await { return ItemStack::EMPTY.clone(); } } else if !self.insert_item(&mut slot_stack, 0, 1, false).await { return ItemStack::EMPTY.clone(); } } if slot_stack.is_empty() { slot.set_stack(ItemStack::EMPTY.clone()).await; } else { slot.set_stack(slot_stack).await; } // CRITICAL FIX: Ensure the client is notified when shift-clicking items into the slots self.update_enchantments(player).await; } stack_left }) } fn on_slot_click<'a>( &'a mut self, slot_index: i32, button: i32, action_type: pumpkin_protocol::java::server::play::SlotActionType, player: &'a dyn InventoryPlayer, ) -> ScreenHandlerFuture<'a, ()> { Box::pin(async move { self.internal_on_slot_click(slot_index, button, action_type, player) .await; if slot_index == 0 || slot_index == 1 { self.update_enchantments(player).await; } }) } } #[cfg(test)] mod tests { use super::*; #[test] fn lapis_slot_only_accepts_lapis_lazuli() { assert!(is_lapis(&ItemStack::new(1, &Item::LAPIS_LAZULI))); assert!(!is_lapis(&ItemStack::new(1, &Item::DIRT))); } }