Skip to main content

steel_core/inventory/menu/kinds/
furnace_menu.rs

1//! Furnace, blast-furnace, and smoker menus.
2
3use std::array::from_fn;
4
5use steel_registry::recipe::SingleItemRecipeInput;
6use steel_registry::{REGISTRY, menu_type::MenuTypeRef, vanilla_menu_types};
7use steel_utils::{DowncastType, DowncastTypeKey, locks::Shared};
8
9use crate::block_entity::entities::{FurnaceContainer, FurnaceKind};
10use crate::inventory::fuel_values::VANILLA_FUEL_VALUES;
11use crate::inventory::menu::builder::SectionKind;
12use crate::inventory::prelude::*;
13use crate::inventory::slots::{FurnaceFuelSlot, FurnaceResultSlot};
14use crate::player::player_inventory::PlayerInventory;
15
16/// Builds the three-slot Vanilla menu for one furnace-family block entity.
17#[must_use]
18pub fn furnace_menu(
19    inventory: Shared<PlayerInventory>,
20    container_id: u8,
21    container: ContainerRef,
22    kind: FurnaceKind,
23) -> Menu {
24    let mut builder = MenuBuilder::new(menu_type(kind), container_id);
25
26    let input = builder.section_at(container.clone(), [0], SectionKind::Normal);
27    let fuel = builder.section_at(
28        container.clone(),
29        [1],
30        SectionKind::custom(|container, index| {
31            Box::new(FurnaceFuelSlot::new(container.clone(), index))
32        }),
33    );
34    let result = builder.section_at(
35        container.clone(),
36        [2],
37        SectionKind::custom(|container, index| {
38            Box::new(FurnaceResultSlot::new(container.clone(), index))
39        }),
40    );
41    let player = builder.player_inventory(&inventory);
42    let data = from_fn(|_| builder.data_slot(0));
43
44    builder.build(FurnaceMenuKind {
45        container,
46        kind,
47        input,
48        fuel,
49        result,
50        player,
51        data,
52    })
53}
54
55#[must_use]
56const fn menu_type(kind: FurnaceKind) -> MenuTypeRef {
57    match kind {
58        FurnaceKind::Furnace => &vanilla_menu_types::FURNACE,
59        FurnaceKind::BlastFurnace => &vanilla_menu_types::BLAST_FURNACE,
60        FurnaceKind::Smoker => &vanilla_menu_types::SMOKER,
61    }
62}
63
64/// State and shift-click behavior shared by all furnace-family menus.
65pub struct FurnaceMenuKind {
66    container: ContainerRef,
67    kind: FurnaceKind,
68    input: Section,
69    fuel: Section,
70    result: Section,
71    player: PlayerInventorySections,
72    data: [DataSlot; 4],
73}
74
75// SAFETY: This Steel-owned key uniquely identifies furnace-family menu behavior.
76unsafe impl DowncastType for FurnaceMenuKind {
77    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:menu/abstract_furnace");
78}
79
80impl FurnaceMenuKind {
81    fn update_data(&self, behavior: &mut MenuBehavior, guard: &ContainerLockGuard) {
82        let Some(container) = guard.get_typed::<FurnaceContainer>(self.container.container_id())
83        else {
84            return;
85        };
86        for (slot, value) in self.data.into_iter().zip(container.data()) {
87            slot.set(behavior, value);
88        }
89    }
90
91    fn can_smelt(&self, stack: &ItemStack) -> bool {
92        REGISTRY
93            .recipes
94            .find_match(
95                self.kind.recipe_type(),
96                &SingleItemRecipeInput::new(stack.clone()),
97            )
98            .is_some()
99    }
100}
101
102impl MenuKind for FurnaceMenuKind {
103    fn still_valid(&self, _behavior: &MenuBehavior, player: &Player) -> bool {
104        self.container.still_valid(player)
105    }
106
107    fn on_open(
108        &mut self,
109        behavior: &mut MenuBehavior,
110        guard: &mut ContainerLockGuard,
111        _player: &Player,
112    ) {
113        self.update_data(behavior, guard);
114    }
115
116    fn on_tick(
117        &mut self,
118        behavior: &mut MenuBehavior,
119        guard: &mut ContainerLockGuard,
120        _player: &Player,
121    ) {
122        self.update_data(behavior, guard);
123    }
124
125    fn quick_move(
126        &mut self,
127        behavior: &mut MenuBehavior,
128        guard: &mut ContainerLockGuard,
129        slot_index: usize,
130        player: &Player,
131    ) -> Option<ItemStack> {
132        if slot_index >= behavior.slots().len() {
133            return Some(ItemStack::empty());
134        }
135        let clicked = behavior.slots()[slot_index].get_item(guard).clone();
136        if clicked.is_empty() {
137            return Some(ItemStack::empty());
138        }
139        let mut remaining = clicked.clone();
140
141        let moved = if self.result.contains(slot_index) {
142            behavior.move_item_stack_to(
143                guard,
144                slot_index,
145                &mut remaining,
146                self.player.all().start(),
147                self.player.all().end(),
148                FillDirection::Backward,
149            )
150        } else if self.input.contains(slot_index) || self.fuel.contains(slot_index) {
151            behavior.move_item_stack_to(
152                guard,
153                slot_index,
154                &mut remaining,
155                self.player.all().start(),
156                self.player.all().end(),
157                FillDirection::Forward,
158            )
159        } else if self.can_smelt(&clicked) {
160            behavior.move_item_stack_to(
161                guard,
162                slot_index,
163                &mut remaining,
164                self.input.start(),
165                self.input.end(),
166                FillDirection::Forward,
167            )
168        } else if VANILLA_FUEL_VALUES.is_fuel(clicked.item()) {
169            behavior.move_item_stack_to(
170                guard,
171                slot_index,
172                &mut remaining,
173                self.fuel.start(),
174                self.fuel.end(),
175                FillDirection::Forward,
176            )
177        } else if self.player.main().contains(slot_index) {
178            behavior.move_item_stack_to(
179                guard,
180                slot_index,
181                &mut remaining,
182                self.player.hotbar().start(),
183                self.player.hotbar().end(),
184                FillDirection::Forward,
185            )
186        } else if self.player.hotbar().contains(slot_index) {
187            behavior.move_item_stack_to(
188                guard,
189                slot_index,
190                &mut remaining,
191                self.player.main().start(),
192                self.player.main().end(),
193                FillDirection::Forward,
194            )
195        } else {
196            false
197        };
198
199        if !moved {
200            return Some(ItemStack::empty());
201        }
202        behavior.update_quick_move_source(guard, slot_index, &remaining, &clicked);
203        if remaining.count() == clicked.count() {
204            return Some(ItemStack::empty());
205        }
206        if let Some(remainder) = behavior.slots()[slot_index].on_take(guard, &remaining, player) {
207            player.add_item_or_drop_with_guard(guard, remainder);
208        }
209        Some(clicked)
210    }
211
212    fn can_take_item_for_pick_all(&self, _carried: &ItemStack, slot_index: usize) -> bool {
213        !self.result.contains(slot_index)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use steel_registry::{init_vanilla_registry, vanilla_items};
220    use steel_utils::locks::IntoShared as _;
221
222    use super::*;
223    use crate::inventory::container::SimpleContainer;
224
225    #[test]
226    fn manual_input_accepts_items_without_a_matching_recipe() {
227        init_vanilla_registry();
228
229        for (kind, item) in [
230            (FurnaceKind::Furnace, &vanilla_items::FURNACE),
231            (FurnaceKind::Smoker, &vanilla_items::RAW_IRON),
232        ] {
233            let stack = ItemStack::new(item);
234            assert!(
235                REGISTRY
236                    .recipes
237                    .find_match(
238                        kind.recipe_type(),
239                        &SingleItemRecipeInput::new(stack.clone()),
240                    )
241                    .is_none()
242            );
243
244            let menu = furnace_menu(
245                PlayerInventory::new().into_shared(),
246                1,
247                ContainerRef::from(SimpleContainer::new(3).into_shared()),
248                kind,
249            );
250
251            assert!(menu.behavior().slots()[0].may_place(&stack));
252        }
253    }
254}