Skip to main content

steel_core/inventory/slots/
crafting_slots.rs

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