Skip to main content

steel_core/block_entity/entities/
campfire.rs

1//! Campfire cooking block entity.
2
3use std::array::from_fn;
4use std::mem;
5use std::sync::{Arc, Weak};
6
7use simdnbt::ToNbtTag as _;
8use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
9use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
10use steel_registry::blocks::block_state_ext::BlockStateExt as _;
11use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
12use steel_registry::item_stack::ItemStack;
13use steel_registry::recipe::{SingleItemRecipeInput, vanilla_recipe_types};
14use steel_registry::{REGISTRY, vanilla_block_entity_types, vanilla_game_events};
15use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex};
16
17use crate::block_entity::{BlockEntity, BlockEntityBase};
18use crate::entity::Entity;
19use crate::player::Player;
20use crate::world::World;
21use crate::world::game_event::GameEventContext;
22
23/// Number of independently cooking item positions on a campfire.
24pub const CAMPFIRE_SLOTS: usize = 4;
25const BURN_COOL_SPEED: i32 = 2;
26const LIT: &BoolProperty = &BlockStateProperties::LIT;
27
28pub struct CampfireCookingState {
29    items: [ItemStack; CAMPFIRE_SLOTS],
30    cooking_progress: [i32; CAMPFIRE_SLOTS],
31    cooking_time: [i32; CAMPFIRE_SLOTS],
32}
33
34impl CampfireCookingState {
35    fn new() -> Self {
36        Self {
37            items: from_fn(|_| ItemStack::empty()),
38            cooking_progress: [0; CAMPFIRE_SLOTS],
39            cooking_time: [0; CAMPFIRE_SLOTS],
40        }
41    }
42}
43
44/// Stores four independently cooking campfire items.
45pub struct CampfireBlockEntity {
46    base: Arc<BlockEntityBase>,
47    cooking: SyncMutex<CampfireCookingState>,
48}
49
50// SAFETY: This Steel-owned key uniquely identifies `CampfireBlockEntity`.
51unsafe impl DowncastType for CampfireBlockEntity {
52    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/campfire");
53}
54
55impl CampfireBlockEntity {
56    /// Creates a campfire block entity.
57    #[must_use]
58    pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
59        Self {
60            base: Arc::new(BlockEntityBase::new(
61                &vanilla_block_entity_types::CAMPFIRE,
62                level,
63                pos,
64                state,
65            )),
66            cooking: SyncMutex::new(CampfireCookingState::new()),
67        }
68    }
69
70    /// Inserts one item when a campfire recipe exists and an empty cooking slot is available.
71    pub fn place_food(&self, player: &Player, stack: ItemStack) -> bool {
72        let input = SingleItemRecipeInput::new(stack.clone());
73        let Some(recipe) = REGISTRY
74            .recipes
75            .find_match(&vanilla_recipe_types::CAMPFIRE_COOKING, &input)
76        else {
77            return false;
78        };
79        {
80            let mut cooking = self.cooking.lock();
81            let Some(slot) = cooking.items.iter().position(ItemStack::is_empty) else {
82                return false;
83            };
84            cooking.cooking_time[slot] = recipe.data().cooking_time;
85            cooking.cooking_progress[slot] = 0;
86            cooking.items[slot] = stack;
87        }
88
89        self.set_changed();
90        let Some(world) = self.get_level() else {
91            return true;
92        };
93        world.game_event(
94            &vanilla_game_events::BLOCK_CHANGE,
95            self.get_block_pos(),
96            &GameEventContext::new(Some(player as &dyn Entity), Some(self.get_block_state())),
97        );
98        world.send_block_updated(self.get_block_pos());
99        true
100    }
101
102    fn cook_tick(&self, world: &Arc<World>) {
103        let pos = self.get_block_pos();
104        let state = self.get_block_state();
105        let mut completed = Vec::new();
106        let changed = {
107            let mut cooking = self.cooking.lock();
108            let mut changed = false;
109            for slot in 0..CAMPFIRE_SLOTS {
110                if cooking.items[slot].is_empty() {
111                    continue;
112                }
113                changed = true;
114                cooking.cooking_progress[slot] += 1;
115                if cooking.cooking_progress[slot] < cooking.cooking_time[slot] {
116                    continue;
117                }
118
119                let item = mem::take(&mut cooking.items[slot]);
120                let input = SingleItemRecipeInput::new(item.clone());
121                let result = REGISTRY
122                    .recipes
123                    .find_match(&vanilla_recipe_types::CAMPFIRE_COOKING, &input)
124                    .map_or(item, |recipe| recipe.data().result.create());
125                completed.push(result);
126            }
127            changed
128        };
129
130        for result in completed {
131            world.drop_item_stack(pos, result);
132            world.send_block_updated(pos);
133            world.game_event(
134                &vanilla_game_events::BLOCK_CHANGE,
135                pos,
136                &GameEventContext::new(None, Some(state)),
137            );
138        }
139        if changed {
140            self.set_changed();
141        }
142    }
143
144    fn cooldown_tick(&self) {
145        let changed = {
146            let mut cooking = self.cooking.lock();
147            let mut changed = false;
148            for slot in 0..CAMPFIRE_SLOTS {
149                if cooking.cooking_progress[slot] > 0 {
150                    cooking.cooking_progress[slot] = (cooking.cooking_progress[slot]
151                        - BURN_COOL_SPEED)
152                        .clamp(0, cooking.cooking_time[slot]);
153                    changed = true;
154                }
155            }
156            changed
157        };
158        if changed {
159            self.set_changed();
160        }
161    }
162
163    fn save_items(cooking: &CampfireCookingState) -> NbtList {
164        let mut items = Vec::new();
165        for (slot, stack) in cooking.items.iter().enumerate() {
166            if !stack.is_empty()
167                && let NbtTag::Compound(mut item) = stack.clone().to_nbt_tag()
168            {
169                item.insert("Slot", slot as i8);
170                items.push(item);
171            }
172        }
173        NbtList::Compound(items)
174    }
175}
176
177impl BlockEntity for CampfireBlockEntity {
178    fn base(&self) -> &BlockEntityBase {
179        &self.base
180    }
181
182    fn pre_remove_side_effects(&self, pos: BlockPos, _state: BlockStateId) {
183        let items = {
184            let mut cooking = self.cooking.lock();
185            mem::replace(&mut cooking.items, from_fn(|_| ItemStack::empty()))
186        };
187        let Some(world) = self.get_level() else {
188            return;
189        };
190        for item in items {
191            world.drop_item_stack(pos, item);
192        }
193    }
194
195    fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
196        let nbt: NbtCompoundView<'_, '_> = nbt.into();
197        let mut cooking = self.cooking.lock();
198        cooking.items.fill(ItemStack::empty());
199        if let Some(items) = nbt.list("Items").and_then(|list| list.compounds()) {
200            for compound in items {
201                let Some(slot) = compound.byte("Slot").map(|slot| slot as usize) else {
202                    continue;
203                };
204                if slot < CAMPFIRE_SLOTS
205                    && let Some(stack) = ItemStack::from_borrowed_compound(&compound)
206                {
207                    cooking.items[slot] = stack;
208                }
209            }
210        }
211        cooking.cooking_progress = nbt
212            .int_array("CookingTimes")
213            .map_or([0; CAMPFIRE_SLOTS], |values| {
214                from_fn(|slot| values.get(slot).copied().unwrap_or(0))
215            });
216        cooking.cooking_time = nbt
217            .int_array("CookingTotalTimes")
218            .map_or([0; CAMPFIRE_SLOTS], |values| {
219                from_fn(|slot| values.get(slot).copied().unwrap_or(0))
220            });
221    }
222
223    fn save_additional(&self, nbt: &mut NbtCompound) {
224        let cooking = self.cooking.lock();
225        nbt.insert("Items", Self::save_items(&cooking));
226        nbt.insert(
227            "CookingTimes",
228            NbtTag::IntArray(cooking.cooking_progress.to_vec()),
229        );
230        nbt.insert(
231            "CookingTotalTimes",
232            NbtTag::IntArray(cooking.cooking_time.to_vec()),
233        );
234    }
235
236    fn get_update_tag(&self) -> Option<NbtCompound> {
237        let mut nbt = NbtCompound::new();
238        nbt.insert("Items", Self::save_items(&self.cooking.lock()));
239        Some(nbt)
240    }
241
242    fn tick(&self, world: &Arc<World>) {
243        if self.get_block_state().get_value(LIT) {
244            self.cook_tick(world);
245        } else {
246            self.cooldown_tick();
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use std::io::Cursor;
254
255    use simdnbt::borrow::read_compound;
256    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_items};
257
258    use super::*;
259
260    fn campfire() -> CampfireBlockEntity {
261        init_vanilla_registry();
262        CampfireBlockEntity::new(
263            Weak::new(),
264            BlockPos::new(4, 70, -2),
265            vanilla_blocks::CAMPFIRE.default_state(),
266        )
267    }
268
269    #[test]
270    fn campfire_round_trips_all_four_progress_tracks_with_items() {
271        let source = campfire();
272        {
273            let mut cooking = source.cooking.lock();
274            cooking.items[2] = ItemStack::new(&vanilla_items::BEEF);
275            cooking.cooking_progress = [0, 3, 17, 0];
276            cooking.cooking_time = [0, 100, 600, 0];
277        }
278        let mut saved = NbtCompound::new();
279        source.save_additional(&mut saved);
280
281        let mut bytes = Vec::new();
282        saved.write(&mut bytes);
283        let borrowed = read_compound(&mut Cursor::new(bytes.as_slice()))
284            .expect("test campfire NBT should reborrow");
285        let loaded = campfire();
286        loaded.load_additional(&borrowed);
287
288        let cooking = loaded.cooking.lock();
289        assert!(cooking.items[2].is(&vanilla_items::BEEF));
290        assert_eq!(cooking.cooking_progress, [0, 3, 17, 0]);
291        assert_eq!(cooking.cooking_time, [0, 100, 600, 0]);
292    }
293}