steel_core/inventory/menu/kinds/
anvil_menu.rs1use std::sync::{
3 Arc,
4 atomic::{AtomicBool, AtomicI32, Ordering},
5};
6
7use steel_registry::{
8 REGISTRY, RegistryExt, TaggedRegistryExt,
9 blocks::block_state_ext::BlockStateExt,
10 data_components::{
11 components::ItemEnchantments,
12 vanilla_components::{CUSTOM_NAME, ENCHANTMENTS, REPAIR_COST, STORED_ENCHANTMENTS},
13 },
14 enchantment::Enchantment,
15 item_stack::ItemStack,
16 vanilla_block_tags::BlockTag,
17 vanilla_items, vanilla_menu_types,
18};
19use steel_utils::{
20 BlockPos, Identifier, java,
21 locks::{IntoShared, Shared, SyncMutex},
22 text::DisplayResolutor,
23};
24use text_components::TextComponent;
25
26use crate::{
27 behavior::ITEM_BEHAVIORS,
28 inventory::{
29 container::{ResultContainer, SimpleContainer},
30 prelude::*,
31 slots::AnvilResultHandler,
32 },
33 player::player_inventory::PlayerInventory,
34 world::World,
35};
36
37#[must_use]
39pub fn anvil(
40 inventory: Shared<PlayerInventory>,
41 container_id: u8,
42 pos: BlockPos,
43 world: &Arc<World>,
44) -> Menu {
45 let input_container = SimpleContainer::new(2).into_shared();
46 let repair_item_count = Arc::new(AtomicI32::new(0));
47 let level_cost = Arc::new(AtomicI32::new(0));
48 let only_renaming = Arc::new(AtomicBool::new(false));
49
50 let result_container = ResultContainer::new().into_shared();
51
52 let mut builder = MenuBuilder::new(&vanilla_menu_types::ANVIL, container_id);
53
54 let input = builder.section_all(&input_container);
55 let result = builder.result_slot(AnvilResultHandler::new(
56 input_container.clone(),
57 result_container.clone(),
58 repair_item_count.clone(),
59 level_cost.clone(),
60 only_renaming.clone(),
61 pos,
62 world.clone(),
63 ));
64
65 let player = builder.player_inventory(&inventory);
66
67 let level_cost_data_slot = builder.data_slot(0);
68
69 builder.route_with_remainder_policy(
70 result,
71 player.all(),
72 FillDirection::Backward,
73 FakeResultRemainderPolicy::Discard,
74 );
75 builder.route(input, player.all(), FillDirection::Forward);
76 builder.route(player.hotbar(), input, FillDirection::Forward);
77 builder.route(player.main(), input, FillDirection::Forward);
78 builder.drain(input);
79
80 builder.build(AnvilKind {
81 input_container,
82 result_container,
83 block_pos: pos,
84 world: Arc::clone(world),
85 repair_item_count,
86 level_cost: level_cost_data_slot,
87 level_cost_value: level_cost,
88 only_renaming,
89 item_name: SyncMutex::new(None),
90 })
91}
92
93pub struct AnvilKind {
95 input_container: Shared<SimpleContainer>,
97 result_container: Shared<ResultContainer>,
99 block_pos: BlockPos,
100 world: Arc<World>,
101 repair_item_count: Arc<AtomicI32>,
102 level_cost: DataSlot,
104 level_cost_value: Arc<AtomicI32>,
106 only_renaming: Arc<AtomicBool>,
108 item_name: SyncMutex<Option<String>>,
109}
110
111unsafe impl steel_utils::DowncastType for AnvilKind {
114 const TYPE_KEY: steel_utils::DowncastTypeKey =
115 steel_utils::DowncastTypeKey::new("steel:menu/anvil");
116}
117
118impl AnvilKind {
119 fn set_cost(&mut self, behavior: &mut MenuBehavior, cost: i32) {
122 self.level_cost.set(behavior, Self::client_cost(cost));
123 self.level_cost_value.store(cost, Ordering::Relaxed);
124 }
125
126 const fn client_cost(cost: i32) -> i16 {
127 let [low, high, _, _] = cost.to_le_bytes();
128 i16::from_le_bytes([low, high])
129 }
130
131 #[tracing::instrument(skip(self, behavior, player, guard), level = "info", fields(player = %player.gameprofile.name))]
136 #[expect(
137 clippy::too_many_lines,
138 reason = "mirrors Vanilla's ordered createResult flow in one auditable calculation"
139 )]
140 pub(crate) fn create_result(
141 &mut self,
142 behavior: &mut MenuBehavior,
143 guard: &mut ContainerLockGuard,
144 player: &Player,
145 ) {
146 let Some([input_container, result_container]) = guard.get_disjoint_mut([
147 ContainerId::from_arc(&self.input_container),
148 ContainerId::from_arc(&self.result_container),
149 ]) else {
150 panic!("failed to lock input and/or result containers to create anvil result")
151 };
152
153 let [first, second] = input_container.items() else {
154 panic!("input_container in anvil menu does not fit expected shape")
155 };
156
157 let mut additional_cost = 0_u32;
158 let mut rename_cost = 0_i32;
159 self.only_renaming.store(false, Ordering::Relaxed);
160 self.set_cost(behavior, 0);
161
162 if first.is_empty() || !Self::can_store_enchantments(first) {
163 result_container.set_item(0, ItemStack::empty());
164 self.set_cost(behavior, 0);
165 return;
166 }
167
168 self.repair_item_count.store(0, Ordering::Relaxed);
169
170 let mut result = first.clone();
171 let mut enchantments = first
172 .get_enchantments_for_crafting()
173 .cloned()
174 .unwrap_or_default();
175 let prior_repair_cost: i64 = i64::from(*first.get(REPAIR_COST).unwrap_or(&0))
176 + i64::from(*second.get(REPAIR_COST).unwrap_or(&0));
177
178 if !second.is_empty() {
179 let has_stored_enchantments = second.has(STORED_ENCHANTMENTS);
180
181 if result.is_damageable_item() && first.is_valid_repair_item(second) {
182 let mut repair_per_unit =
183 result.get_damage_value().min(result.get_max_damage() / 4);
184 if repair_per_unit <= 0 {
185 result_container.set_item(0, ItemStack::empty());
186 self.set_cost(behavior, 0);
187 return;
188 }
189
190 let mut materials_used = 0;
191 while repair_per_unit > 0 && materials_used < second.count {
192 let new_damage = result.get_damage_value() - repair_per_unit;
193 result.set_damage_value(new_damage);
194 additional_cost += 1;
195 materials_used += 1;
196 repair_per_unit = result.get_damage_value().min(result.get_max_damage() / 4);
197 }
198
199 self.repair_item_count
200 .store(materials_used, Ordering::Relaxed);
201 } else {
202 if !has_stored_enchantments
203 && (!result.is(second.item) || !result.is_damageable_item())
204 {
205 result_container.set_item(0, ItemStack::empty());
206 self.set_cost(behavior, 0);
207 return;
208 }
209
210 if result.is_damageable_item() && !has_stored_enchantments {
211 let first_durability = first.get_max_damage() - first.get_damage_value();
213 let second_durability = second.get_max_damage() - second.get_damage_value();
214 let durability_bonus = second_durability + result.get_max_damage() * 12 / 100;
215 let total_durability = first_durability + durability_bonus;
216 let new_damage = (result.get_max_damage() - total_durability).max(0);
217
218 if new_damage < result.get_damage_value() {
219 result.set_damage_value(new_damage);
220 additional_cost += 2;
221 }
222 }
223
224 let sacrifice_enchantments: ItemEnchantments = second
226 .get_enchantments_for_crafting()
227 .cloned()
228 .unwrap_or_default();
229 let mut any_compatible = false;
230 let mut any_incompatible = false;
231
232 for (ident, level) in sacrifice_enchantments {
233 let existing_level = enchantments.get_level(&ident);
234 let mut merged_level: u32 = if existing_level == level {
235 level + 1
236 } else {
237 existing_level.max(level)
238 };
239
240 let enchantment = REGISTRY
241 .enchantments
242 .by_key(&ident)
243 .expect("should exist because we got it from item enchantments");
244 let mut can_apply = enchantment.can_enchant(first.item)
245 || first.is(&vanilla_items::ENCHANTED_BOOK)
246 || player.has_infinite_materials();
247
248 for (existing_key, _) in enchantments.iter() {
249 if *existing_key == enchantment.key {
250 continue;
251 }
252 let Some(existing) = REGISTRY.enchantments.by_key(existing_key) else {
253 continue;
254 };
255 if !Enchantment::are_compatible(enchantment, existing) {
256 can_apply = false;
257 additional_cost += 1;
258 }
259 }
260
261 if can_apply {
262 any_compatible = true;
263 merged_level = merged_level.min(enchantment.max_level);
264 enchantments.set(ident, merged_level);
265
266 let mut anvil_cost: i32 = enchantment.anvil_cost;
267 if has_stored_enchantments {
268 anvil_cost = (anvil_cost / 2).max(1);
269 }
270 additional_cost += anvil_cost as u32 * merged_level;
271
272 if first.count > 1 {
273 additional_cost = 40;
274 }
275 } else {
276 any_incompatible = true;
277 }
278 }
279
280 if any_incompatible && !any_compatible {
281 result_container.set_item(0, ItemStack::empty());
282 self.set_cost(behavior, 0);
283 return;
284 }
285 }
286 }
287
288 let item_name = self.item_name.lock();
290 if let Some(name) = item_name.as_deref().filter(|name| !java::is_blank(name)) {
291 if name != ITEM_BEHAVIORS.hover_name(first).to_plain(&DisplayResolutor) {
292 rename_cost = 1;
293 additional_cost += rename_cost as u32;
294 result.set(CUSTOM_NAME, TextComponent::from(name.to_string()));
295 }
296 } else if first.has(CUSTOM_NAME) {
297 rename_cost = 1;
298 additional_cost += rename_cost as u32;
299 result.remove(CUSTOM_NAME);
300 }
301 drop(item_name);
302
303 let total_cost = if additional_cost == 0 {
305 0
306 } else {
307 (prior_repair_cost + i64::from(additional_cost)).clamp(0, i64::from(i32::MAX)) as i32
308 };
309 self.set_cost(behavior, total_cost);
310
311 if additional_cost == 0 {
312 result = ItemStack::empty();
313 }
314
315 let only_renaming = rename_cost == additional_cost as i32 && rename_cost > 0;
316 self.only_renaming.store(only_renaming, Ordering::Relaxed);
317 if only_renaming && total_cost >= 40 {
318 self.set_cost(behavior, 39);
319 }
320
321 if total_cost >= 40 && !only_renaming && !player.has_infinite_materials() {
322 result = ItemStack::empty();
323 }
324
325 if !result.is_empty() {
327 let second_repair_cost = *second.get(REPAIR_COST).unwrap_or(&0);
328 let mut final_repair_cost = *result.get(REPAIR_COST).unwrap_or(&0);
329 if final_repair_cost < second_repair_cost {
330 final_repair_cost = second_repair_cost;
331 }
332 if rename_cost != additional_cost as i32 || rename_cost == 0 {
333 final_repair_cost = Self::calculate_increased_repair_cost(final_repair_cost);
334 }
335 result.set(REPAIR_COST, final_repair_cost);
336 let enchantments: Vec<(Identifier, u32)> =
337 enchantments.iter().map(|(k, v)| (k.clone(), *v)).collect();
338 result.set_enchantments(&enchantments, false);
339 }
340
341 result_container.set_item(0, result.clone());
342 }
343
344 fn validate_item_name(name: String) -> Option<String> {
345 let filtered = name
346 .chars()
347 .filter(|char| char != &'ยง' && char >= &' ' && char != &'\x7F')
348 .collect::<String>();
349 (filtered.encode_utf16().count() <= 50).then_some(filtered)
350 }
351
352 fn can_store_enchantments(item_stack: &ItemStack) -> bool {
353 item_stack.has(if item_stack.is(&vanilla_items::ENCHANTED_BOOK) {
354 STORED_ENCHANTMENTS
355 } else {
356 ENCHANTMENTS
357 })
358 }
359
360 const fn calculate_increased_repair_cost(old_repair_cost: i32) -> i32 {
361 old_repair_cost.saturating_mul(2).saturating_add(1)
362 }
363}
364
365impl MenuKind for AnvilKind {
366 fn still_valid(&self, _behavior: &MenuBehavior, player: &Player) -> bool {
368 let state = self.world.get_block_state(self.block_pos);
369 REGISTRY
370 .blocks
371 .is_in_tag(state.get_block(), &BlockTag::ANVIL)
372 && player.is_within_block_interaction_range_with_buffer(self.block_pos, 4.0)
373 }
374
375 fn slots_changed(
376 &mut self,
377 behavior: &mut MenuBehavior,
378 guard: &mut ContainerLockGuard,
379 player: &Player,
380 ) {
381 self.create_result(behavior, guard, player);
382 }
383
384 fn removed(&mut self, _behavior: &mut MenuBehavior, _player: &Player) {
386 self.result_container.lock().set_item(0, ItemStack::empty());
387 }
388
389 #[tracing::instrument(skip(self, behavior, player), level = "info")]
391 fn on_rename(&mut self, behavior: &mut MenuBehavior, name: String, player: &Player) {
392 let Some(validated_name) = Self::validate_item_name(name) else {
393 return;
394 };
395
396 {
397 let mut item_name_guard = self.item_name.lock();
398 match &*item_name_guard {
399 Some(current) if *current == validated_name => return,
400 _ => *item_name_guard = Some(validated_name),
401 }
402 }
403
404 {
405 let mut guard = behavior.lock_all_containers();
406 self.create_result(behavior, &mut guard, player);
407 }
408 behavior.broadcast_changes(&player.connection);
409 }
410}
411
412#[cfg(test)]
413mod tests;