Skip to main content

steel_core/inventory/slots/
crafting_slots.rs

1use steel_registry::item_stack::ItemStack;
2use steel_utils::locks::Shared;
3
4use crate::inventory::container::{CraftingContainer, ResultContainer};
5use crate::{
6    inventory::{
7        container::Container,
8        lock::{ContainerId, ContainerLockGuard, ContainerRef},
9        recipe_manager,
10        slots::result_handler::ResultHandler,
11    },
12    player::Player,
13};
14
15/// A Recipe Handler for Crafting Recipes
16#[derive(Clone)]
17pub struct CraftingHandler {
18    crafting_container: Shared<CraftingContainer>,
19    result_container: Shared<ResultContainer>,
20    grid_size: usize,
21}
22
23impl CraftingHandler {
24    /// Creates a new Crafting Recipe Handler
25    pub const fn new(
26        crafting_container: Shared<CraftingContainer>,
27        result_container: Shared<ResultContainer>,
28        grid_size: usize,
29    ) -> Self {
30        Self {
31            crafting_container,
32            result_container,
33            grid_size,
34        }
35    }
36
37    /// The `ContainerId` of the crafting container
38    #[must_use]
39    pub fn crafting_id(&self) -> ContainerId {
40        ContainerId::from_arc(&self.crafting_container)
41    }
42
43    /// A shared handle to the crafting container.
44    #[must_use]
45    pub fn crafting_container(&self) -> Shared<CraftingContainer> {
46        self.crafting_container.clone()
47    }
48
49    /// The `ContainerId` of the result container
50    #[must_use]
51    pub fn result_id(&self) -> ContainerId {
52        ContainerId::from_arc(&self.result_container)
53    }
54}
55
56impl ResultHandler for CraftingHandler {
57    fn result_container(&self) -> ContainerRef {
58        ContainerRef::from(self.result_container.clone())
59    }
60
61    fn dependencies(&self) -> Vec<ContainerRef> {
62        vec![ContainerRef::from(self.crafting_container.clone())]
63    }
64
65    fn update_result(&self, guard: &mut ContainerLockGuard) {
66        // TODO: Enforce limited crafting and retain the recipe once player recipe books exist.
67        let crafting = guard
68            .get_typed::<CraftingContainer>(self.crafting_id())
69            .expect("crafting container not locked");
70
71        let result_stack =
72            recipe_manager::assemble_for_container(crafting).unwrap_or_else(ItemStack::empty);
73
74        let result_container = guard
75            .get_typed_mut::<ResultContainer>(self.result_id())
76            .expect("result container not locked");
77        result_container.set_item(0, result_stack);
78        result_container.set_changed();
79    }
80
81    fn on_result_taken(
82        &self,
83        guard: &mut ContainerLockGuard,
84        player: &Player,
85    ) -> Option<ItemStack> {
86        // TODO: Unlock the recipe and trigger RECIPE_CRAFTED once their foundations exist.
87        let mut remainder_overflow: Vec<ItemStack> = Vec::new();
88
89        let remainders_and_positioned = {
90            let crafting = guard
91                .get_typed::<CraftingContainer>(self.crafting_id())
92                .expect("crafting container not locked");
93            recipe_manager::get_remaining_items(crafting)
94        };
95
96        let Some((remainders, positioned)) = remainders_and_positioned else {
97            guard
98                .get_typed_mut::<ResultContainer>(self.result_id())
99                .expect("result container not locked")
100                .set_item(0, ItemStack::empty());
101            return None;
102        };
103
104        {
105            let crafting = guard
106                .get_typed_mut::<CraftingContainer>(self.crafting_id())
107                .expect("crafting container not locked");
108
109            let input = &positioned.input;
110
111            for y in 0..input.height {
112                for x in 0..input.width {
113                    let grid_slot = positioned.to_grid_slot(x, y, self.grid_size);
114                    let remainder_idx = x + y * input.width;
115                    let replacement = if remainder_idx < remainders.len() {
116                        remainders[remainder_idx].clone()
117                    } else {
118                        ItemStack::empty()
119                    };
120
121                    {
122                        let item = crafting.get_item_mut(grid_slot);
123                        if !item.is_empty() {
124                            item.shrink_one();
125                        }
126                    }
127
128                    if !replacement.is_empty() {
129                        let current_item = crafting.get_item(grid_slot).clone();
130
131                        if current_item.is_empty() {
132                            crafting.set_item(grid_slot, replacement);
133                        } else if ItemStack::is_same_item_same_components(
134                            &current_item,
135                            &replacement,
136                        ) {
137                            crafting.get_item_mut(grid_slot).grow(replacement.count());
138                        } else {
139                            remainder_overflow.push(replacement);
140                        }
141                    }
142                }
143            }
144
145            crafting.set_changed();
146        }
147
148        self.update_result(guard);
149
150        for remainder in remainder_overflow {
151            player.add_item_or_drop_with_guard(guard, remainder);
152        }
153
154        None
155    }
156
157    fn is_result_valid(&self, guard: &ContainerLockGuard, _player: &Player) -> bool {
158        let Some(result) = guard.get(self.result_id()) else {
159            return false;
160        };
161        let result_item = result.get_item(0);
162        if result_item.is_empty() {
163            return false;
164        }
165
166        let Some(crafting) = guard.get_typed::<CraftingContainer>(self.crafting_id()) else {
167            return false;
168        };
169
170        let Some(assembled) = recipe_manager::assemble_for_container(crafting) else {
171            return false;
172        };
173
174        ItemStack::matches(result_item, &assembled)
175    }
176}