Skip to main content

steel_core/inventory/menu/kinds/
anvil_menu.rs

1//! Anvil menu.
2use 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/// Builds the anvil menu.
38#[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
93/// Per-menu anvil state: inputs, result, level cost, and rename text.
94pub struct AnvilKind {
95    /// Input container (two slots).
96    input_container: Shared<SimpleContainer>,
97    /// Result container (single virtual slot).
98    result_container: Shared<ResultContainer>,
99    block_pos: BlockPos,
100    world: Arc<World>,
101    repair_item_count: Arc<AtomicI32>,
102    /// Client-facing level cost data slot.
103    level_cost: DataSlot,
104    /// Level cost shared with [`AnvilResultHandler`], kept in sync with `level_cost`.
105    level_cost_value: Arc<AtomicI32>,
106    /// Whether the current result changes only the first input's name.
107    only_renaming: Arc<AtomicBool>,
108    item_name: SyncMutex<Option<String>>,
109}
110
111// SAFETY: This Steel-owned key uniquely identifies the concrete menu kind
112// within the process.
113unsafe 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    /// Sets the level cost. The client receives the packet's low 16 bits while
120    /// the result handler retains the full server-side cost.
121    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    /// Builds the anvil result from combining and renaming the two inputs.
132    ///
133    /// # Panics
134    /// Panics if the input container is not exactly two slots.
135    #[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                    // Combining two of the same item.
212                    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                // Enchantment merging.
225                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        // Renaming
289        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        // Final cost.
304        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        // Write repair cost to result.
326        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    /// Returns true while the original anvil remains in range.
367    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    /// Clears the virtual result on close. Inputs are drained by [`Menu::removed`].
385    fn removed(&mut self, _behavior: &mut MenuBehavior, _player: &Player) {
386        self.result_container.lock().set_item(0, ItemStack::empty());
387    }
388
389    /// Sets the rename text and recomputes the result with it applied.
390    #[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;