1use std::sync::Arc;
4
5use steel_protocol::packets::game::SoundSource;
6use steel_registry::{
7 REGISTRY, RegistryEntry, TaggedRegistryExt, item_stack::ItemStack, sound_events,
8 vanilla_blocks, vanilla_custom_stats, vanilla_enchantment_tags::EnchantmentTag, vanilla_items,
9 vanilla_menu_types,
10};
11use steel_utils::random::Random as _;
12use steel_utils::random::legacy_random::LegacyRandom;
13use steel_utils::{BlockPos, DowncastType, DowncastTypeKey, locks::Shared};
14
15use crate::behavior::blocks::EnchantingTableBlock;
16use crate::enchantment_helper::{EnchantmentInstance, get_enchantment_cost, select_enchantment};
17use crate::inventory::prelude::*;
18use crate::inventory::slots::EnchantItemSlot;
19use crate::player::player_inventory::{PlayerInventory, Prediction};
20use crate::world::World;
21
22const OFFER_COUNT: usize = 3;
23const ITEM_SLOT: usize = 0;
25const LAPIS_SLOT: usize = 1;
27const TABLE_SLOT_COUNT: usize = 2;
29
30const ENCHANT_SOUND_MIN_PITCH: f32 = 0.9;
31const ENCHANT_SOUND_PITCH_RANGE: f32 = 0.1;
32
33#[must_use]
35pub fn enchantment(
36 inventory: Shared<PlayerInventory>,
37 container_id: u8,
38 pos: BlockPos,
39 world: &Arc<World>,
40 enchantment_seed: i32,
41) -> Menu {
42 let enchant_slots = SimpleContainer::new(TABLE_SLOT_COUNT).into_shared();
43 let enchant_slots_ref = ContainerRef::from(Arc::clone(&enchant_slots));
44
45 let mut builder = MenuBuilder::new(&vanilla_menu_types::ENCHANTMENT, container_id);
46 let item = builder.section_at(
47 enchant_slots_ref.clone(),
48 [ITEM_SLOT],
49 SectionKind::custom(|container, index| {
50 Box::new(EnchantItemSlot::new(container.clone(), index))
51 }),
52 );
53 let lapis = builder.section_at(
54 enchant_slots_ref,
55 [LAPIS_SLOT],
56 SectionKind::restricted(|_, stack| stack.is(&vanilla_items::LAPIS_LAZULI)),
57 );
58 let player_slots = builder.player_inventory(&inventory);
59
60 let cost_slots = [
61 builder.data_slot(0),
62 builder.data_slot(0),
63 builder.data_slot(0),
64 ];
65 let seed_slot = builder.data_slot(EnchantmentKind::client_value(enchantment_seed));
66 let enchant_clue_slots = [
67 builder.data_slot(-1),
68 builder.data_slot(-1),
69 builder.data_slot(-1),
70 ];
71 let level_clue_slots = [
72 builder.data_slot(-1),
73 builder.data_slot(-1),
74 builder.data_slot(-1),
75 ];
76
77 builder.drain([item, lapis]);
78
79 builder.build(EnchantmentKind {
80 enchant_slots,
81 block_pos: pos,
82 world: Arc::clone(world),
83 item,
84 lapis,
85 player_slots,
86 enchantment_seed,
87 table_changes_seen: 0,
88 costs: [0; OFFER_COUNT],
89 enchant_clue: [None; OFFER_COUNT],
90 level_clue: [None; OFFER_COUNT],
91 cost_slots,
92 seed_slot,
93 enchant_clue_slots,
94 level_clue_slots,
95 })
96}
97
98pub struct EnchantmentKind {
101 enchant_slots: Shared<SimpleContainer>,
103 block_pos: BlockPos,
104 world: Arc<World>,
105 item: Section,
106 lapis: Section,
107 player_slots: PlayerInventorySections,
108 enchantment_seed: i32,
109 table_changes_seen: u32,
111 costs: [i32; OFFER_COUNT],
112 enchant_clue: [Option<i32>; OFFER_COUNT],
114 level_clue: [Option<i32>; OFFER_COUNT],
116 cost_slots: [DataSlot; OFFER_COUNT],
117 seed_slot: DataSlot,
118 enchant_clue_slots: [DataSlot; OFFER_COUNT],
119 level_clue_slots: [DataSlot; OFFER_COUNT],
120}
121
122unsafe impl DowncastType for EnchantmentKind {
125 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:menu/enchantment");
126}
127
128impl EnchantmentKind {
129 const fn client_value(value: i32) -> i16 {
131 let [low, high, _, _] = value.to_le_bytes();
132 i16::from_le_bytes([low, high])
133 }
134
135 fn seeded_random(seed: i32) -> LegacyRandom {
136 LegacyRandom::from_seed(i64::from(seed).cast_unsigned())
137 }
138
139 fn sync_data(&self, behavior: &mut MenuBehavior) {
140 for offer in 0..OFFER_COUNT {
141 self.cost_slots[offer].set(behavior, Self::client_value(self.costs[offer]));
142 self.enchant_clue_slots[offer].set(
143 behavior,
144 self.enchant_clue[offer].map_or(-1, Self::client_value),
145 );
146 self.level_clue_slots[offer].set(
147 behavior,
148 self.level_clue[offer].map_or(-1, Self::client_value),
149 );
150 }
151 self.seed_slot
152 .set(behavior, Self::client_value(self.enchantment_seed));
153 }
154
155 const fn clear_offers(&mut self) {
156 self.costs = [0; OFFER_COUNT];
157 self.enchant_clue = [None; OFFER_COUNT];
158 self.level_clue = [None; OFFER_COUNT];
159 }
160
161 fn count_bookshelves(&self) -> i32 {
162 EnchantingTableBlock::BOOKSHELF_OFFSETS
163 .iter()
164 .filter(|offset| {
165 EnchantingTableBlock::is_valid_book_shelf(&self.world, self.block_pos, **offset)
166 })
167 .count() as i32
168 }
169
170 fn enchantment_list(
172 &self,
173 random: &mut LegacyRandom,
174 stack: &ItemStack,
175 slot: usize,
176 enchantment_cost: i32,
177 ) -> Vec<EnchantmentInstance> {
178 random.set_seed(i64::from(self.enchantment_seed.wrapping_add(slot as i32)));
179 let mut list = select_enchantment(
180 random,
181 stack,
182 enchantment_cost,
183 REGISTRY
184 .enchantments
185 .iter_tag(&EnchantmentTag::IN_ENCHANTING_TABLE),
186 );
187 if stack.is(&vanilla_items::BOOK) && list.len() > 1 {
188 let removed = random.next_i32_bounded(list.len() as i32) as usize;
189 list.remove(removed);
190 }
191 list
192 }
193
194 fn table_changes(&self, guard: &ContainerLockGuard) -> Option<u32> {
196 guard
197 .get_typed::<SimpleContainer>(ContainerId::from_arc(&self.enchant_slots))
198 .map(SimpleContainer::times_changed)
199 }
200
201 fn update_offers(&mut self, behavior: &mut MenuBehavior, guard: &mut ContainerLockGuard) {
203 if let Some(changes) = self.table_changes(guard) {
204 self.table_changes_seen = changes;
205 }
206 let stack = behavior.slots()[self.item.start()].get_item(guard).clone();
207 if stack.is_empty() || !stack.is_enchantable() {
208 self.clear_offers();
209 self.sync_data(behavior);
210 return;
211 }
212
213 let bookcases = guard.run_unlocked(|| self.count_bookshelves());
214 let mut random = Self::seeded_random(self.enchantment_seed);
215 for offer in 0..OFFER_COUNT {
216 self.costs[offer] = get_enchantment_cost(&mut random, offer, bookcases, &stack);
217 self.enchant_clue[offer] = None;
218 self.level_clue[offer] = None;
219 if self.costs[offer] < offer as i32 + 1 {
220 self.costs[offer] = 0;
221 }
222 }
223 for offer in 0..OFFER_COUNT {
224 if self.costs[offer] <= 0 {
225 continue;
226 }
227 let list = self.enchantment_list(&mut random, &stack, offer, self.costs[offer]);
228 if list.is_empty() {
229 continue;
230 }
231 let clue = list[random.next_i32_bounded(list.len() as i32) as usize];
232 self.enchant_clue[offer] = Some(clue.enchantment.id() as i32);
233 self.level_clue[offer] = Some(clue.level as i32);
234 }
235 self.sync_data(behavior);
236 }
237}
238
239impl MenuKind for EnchantmentKind {
240 fn still_valid(&self, _behavior: &MenuBehavior, player: &Player) -> bool {
241 still_valid_block(
242 &self.world,
243 self.block_pos,
244 &vanilla_blocks::ENCHANTING_TABLE,
245 player,
246 )
247 }
248
249 fn slots_changed(
251 &mut self,
252 behavior: &mut MenuBehavior,
253 guard: &mut ContainerLockGuard,
254 _player: &Player,
255 ) {
256 if self.table_changes(guard) == Some(self.table_changes_seen) {
257 return;
258 }
259 self.update_offers(behavior, guard);
260 }
261
262 fn click_menu_button(
264 &mut self,
265 behavior: &mut MenuBehavior,
266 button_id: i32,
267 player: &Player,
268 ) -> bool {
269 let Some(offer) = usize::try_from(button_id)
270 .ok()
271 .filter(|offer| *offer < OFFER_COUNT)
272 else {
273 log::debug!(
274 "{} pressed invalid button id: {button_id}",
275 player.gameprofile.name()
276 );
277 return false;
278 };
279 let enchantment_cost = offer as i32 + 1;
280 let has_infinite_materials = player.has_infinite_materials();
281
282 let mut guard = ContainerLockGuard::lock_all(&[ContainerRef::from(&self.enchant_slots)]);
283 let container_id = ContainerId::from_arc(&self.enchant_slots);
284 let Some(container) = guard.get(container_id) else {
285 return false;
286 };
287 let (stack, mut currency) = (
288 container.get_item(ITEM_SLOT).clone(),
289 container.get_item(LAPIS_SLOT).clone(),
290 );
291
292 if (currency.is_empty() || currency.count() < enchantment_cost) && !has_infinite_materials {
293 return false;
294 }
295 let level = player.experience.lock().level();
296 if self.costs[offer] <= 0
297 || stack.is_empty()
298 || ((level < enchantment_cost || level < self.costs[offer]) && !has_infinite_materials)
299 {
300 return false;
301 }
302
303 let mut random = Self::seeded_random(self.enchantment_seed);
304 let new_enchantments = self.enchantment_list(&mut random, &stack, offer, self.costs[offer]);
305 if new_enchantments.is_empty() {
306 return true;
307 }
308
309 {
310 let mut experience = player.experience.lock();
311 experience.on_enchantment_performed(enchantment_cost, rand::random());
312 self.enchantment_seed = experience.enchantment_seed();
313 }
314
315 let mut enchanted = if stack.is(&vanilla_items::BOOK) {
318 ItemStack::with_count_and_patch(
319 &vanilla_items::ENCHANTED_BOOK,
320 stack.count(),
321 stack.components_patch().clone(),
322 )
323 } else {
324 stack
325 };
326 for instance in &new_enchantments {
327 enchanted.upgrade_enchantment(instance.enchantment.key.clone(), instance.level);
328 }
329 currency.consume(enchantment_cost, has_infinite_materials);
330
331 let Some(container) = guard.get_mut(container_id) else {
332 return false;
333 };
334 container.set_item(ITEM_SLOT, enchanted);
335 container.set_item(LAPIS_SLOT, currency);
336 container.set_changed();
337
338 player.award_custom_stat(&vanilla_custom_stats::ENCHANT_ITEM);
339 self.update_offers(behavior, &mut guard);
342 drop(guard);
343
344 self.world.play_sound(
345 &sound_events::BLOCK_ENCHANTMENT_TABLE_USE,
346 SoundSource::Blocks,
347 self.block_pos,
348 1.0,
349 rand::random::<f32>() * ENCHANT_SOUND_PITCH_RANGE + ENCHANT_SOUND_MIN_PITCH,
350 None,
351 );
352 true
353 }
354
355 fn quick_move(
358 &mut self,
359 behavior: &mut MenuBehavior,
360 guard: &mut ContainerLockGuard,
361 slot_index: usize,
362 player: &Player,
363 ) -> Option<ItemStack> {
364 if slot_index >= behavior.slots().len() {
365 return Some(ItemStack::empty());
366 }
367 let clicked = behavior.slots()[slot_index].get_item(guard).clone();
368 if clicked.is_empty() {
369 return Some(ItemStack::empty());
370 }
371 let mut remaining = clicked.clone();
372
373 let moved = if self.item.contains(slot_index) || self.lapis.contains(slot_index) {
374 behavior.move_item_stack_to(
375 guard,
376 slot_index,
377 &mut remaining,
378 self.player_slots.all().start(),
379 self.player_slots.all().end(),
380 FillDirection::Backward,
381 )
382 } else if clicked.is(&vanilla_items::LAPIS_LAZULI) {
383 behavior.move_item_stack_to(
384 guard,
385 slot_index,
386 &mut remaining,
387 self.lapis.start(),
388 self.lapis.end(),
389 FillDirection::Backward,
390 )
391 } else {
392 let item_slot = &behavior.slots()[self.item.start()];
393 if item_slot.has_item(guard) || !item_slot.may_place(&clicked) {
394 return Some(ItemStack::empty());
395 }
396 let single = remaining.split(1);
397 item_slot.set_by_player(guard, single, &ItemStack::empty());
398 true
399 };
400
401 if !moved {
402 return Some(ItemStack::empty());
403 }
404 behavior.update_quick_move_source(guard, slot_index, &remaining, &clicked);
405 if remaining.count() == clicked.count() {
406 return Some(ItemStack::empty());
407 }
408 if let Some(remainder) = behavior.slots()[slot_index].on_take(guard, &remaining, player) {
409 player.add_item_or_drop_with_guard(guard, remainder, Prediction::Predicted);
410 }
411 Some(clicked)
412 }
413}
414
415#[cfg(test)]
416mod tests;