steel_core/block_entity/entities/
campfire.rs1use 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
24pub 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
45pub struct CampfireBlockEntity {
47 base: Arc<BlockEntityBase>,
48 cooking: SyncMutex<CampfireCookingState>,
49}
50
51unsafe impl DowncastType for CampfireBlockEntity {
53 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/campfire");
54}
55
56impl CampfireBlockEntity {
57 #[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 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}