steel_core/inventory/menu/kinds/
furnace_menu.rs1use 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#[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
65pub 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
76unsafe 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}