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