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::player::Player;
20use crate::world::World;
21use crate::world::game_event::GameEventContext;
22
23pub 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
44pub struct CampfireBlockEntity {
46 base: Arc<BlockEntityBase>,
47 cooking: SyncMutex<CampfireCookingState>,
48}
49
50unsafe impl DowncastType for CampfireBlockEntity {
52 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/campfire");
53}
54
55impl CampfireBlockEntity {
56 #[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 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}