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