Skip to main content

steel_core/inventory/menu/kinds/
furnace_menu.rs

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