steel_core/block_entity/entities/
chiseled_bookshelf.rs1use std::io;
4use std::mem;
5use std::sync::{Arc, Weak};
6
7use simdnbt::ToNbtTag;
8use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
9use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
10use steel_registry::ItemStackTemplate;
11use steel_registry::blocks::block_state_ext::BlockStateExt as _;
12use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
13use steel_registry::data_components::components::ItemContainerContents;
14use steel_registry::item_stack::ItemStack;
15use steel_registry::vanilla_block_entity_types;
16use steel_registry::vanilla_item_tags::ItemTag;
17use steel_registry::{vanilla_blocks, vanilla_game_events};
18use steel_utils::types::UpdateFlags;
19use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex};
20
21use crate::block_entity::{BlockEntity, BlockEntityBase};
22use crate::inventory::container::Container;
23use crate::inventory::lock::{ContainerLockGuard, ContainerRef, SharedContainer};
24use crate::world::{LevelReader as _, World, game_event::GameEventContext};
25
26pub const CHISELED_BOOKSHELF_SLOTS: usize = 6;
28
29const DEFAULT_LAST_INTERACTED_SLOT: i32 = -1;
30const ITEMS_NBT_KEY: &str = "Items";
31const ITEM_SLOT_NBT_KEY: &str = "Slot";
32const LAST_INTERACTED_SLOT_NBT_KEY: &str = "last_interacted_slot";
33const MAX_BOOKS_PER_SLOT: i32 = 1;
34
35const OCCUPIED_PROPERTIES: [&BoolProperty; CHISELED_BOOKSHELF_SLOTS] = [
36 &BlockStateProperties::SLOT_0_OCCUPIED,
37 &BlockStateProperties::SLOT_1_OCCUPIED,
38 &BlockStateProperties::SLOT_2_OCCUPIED,
39 &BlockStateProperties::SLOT_3_OCCUPIED,
40 &BlockStateProperties::SLOT_4_OCCUPIED,
41 &BlockStateProperties::SLOT_5_OCCUPIED,
42];
43
44struct ChiseledBookShelfContainer {
45 items: Vec<ItemStack>,
46 last_interacted_slot: i32,
47 state_update_pending: bool,
48}
49
50pub struct ChiseledBookShelfBlockEntity {
52 base: Arc<BlockEntityBase>,
53 container: Arc<SyncMutex<ChiseledBookShelfContainer>>,
54 container_ref: ContainerRef,
55}
56
57unsafe impl DowncastType for ChiseledBookShelfBlockEntity {
60 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/chiseled_bookshelf");
61}
62
63unsafe impl DowncastType for ChiseledBookShelfContainer {
66 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:container/chiseled_bookshelf");
67}
68
69impl ChiseledBookShelfBlockEntity {
70 #[must_use]
72 pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
73 let callback_level = Weak::clone(&level);
74 let base = Arc::new(BlockEntityBase::new(
75 &vanilla_block_entity_types::CHISELED_BOOKSHELF,
76 level,
77 pos,
78 state,
79 ));
80 let container = Arc::new(SyncMutex::new(ChiseledBookShelfContainer {
81 items: (0..CHISELED_BOOKSHELF_SLOTS)
82 .map(|_| ItemStack::empty())
83 .collect(),
84 last_interacted_slot: DEFAULT_LAST_INTERACTED_SLOT,
85 state_update_pending: false,
86 }));
87 let callback_container = Arc::clone(&container);
88 let after_changed = Arc::new(move || {
89 Self::publish_pending_state(&callback_level, pos, &callback_container);
90 });
91 let shared_container: SharedContainer = container.clone();
92
93 Self {
94 container_ref: ContainerRef::owned_by_block_entity_with_callback(
95 shared_container,
96 Arc::clone(&base),
97 after_changed,
98 ),
99 base,
100 container,
101 }
102 }
103
104 fn publish_pending_state(
105 level: &Weak<World>,
106 pos: BlockPos,
107 container: &SyncMutex<ChiseledBookShelfContainer>,
108 ) {
109 let occupied = {
110 let mut container = container.lock();
111 if !container.state_update_pending {
112 return;
113 }
114 container.state_update_pending = false;
115 container
116 .items
117 .iter()
118 .map(|item| !item.is_empty())
119 .collect::<Vec<_>>()
120 };
121
122 let Some(world) = level.upgrade() else {
123 return;
124 };
125 let state = world.get_block_state(pos);
126 if state.get_block() != &vanilla_blocks::CHISELED_BOOKSHELF {
127 return;
128 }
129
130 let mut updated_state = state;
131 for (property, occupied) in OCCUPIED_PROPERTIES.iter().zip(occupied) {
132 updated_state = updated_state.set_value(*property, occupied);
133 }
134
135 world.set_block(pos, updated_state, UpdateFlags::UPDATE_ALL);
136 world.game_event(
137 &vanilla_game_events::BLOCK_CHANGE,
138 pos,
139 &GameEventContext::new(None, Some(updated_state)),
140 );
141 }
142
143 #[must_use]
145 pub fn insert_book(&self, slot: usize, item: ItemStack) -> bool {
146 if slot >= CHISELED_BOOKSHELF_SLOTS
147 || item.is_empty()
148 || !item.item().has_tag(&ItemTag::BOOKSHELF_BOOKS)
149 {
150 return false;
151 }
152
153 let mut guard = ContainerLockGuard::lock_all(&[&self.container_ref]);
154 let Some(container) = guard.get(self.container_ref.container_id()) else {
155 return false;
156 };
157 if !container.get_item(slot).is_empty() {
158 return false;
159 }
160 guard.set_item(self.container_ref.container_id(), slot, item)
161 }
162
163 #[must_use]
165 pub fn remove_book(&self, slot: usize) -> ItemStack {
166 if slot >= CHISELED_BOOKSHELF_SLOTS {
167 return ItemStack::empty();
168 }
169 let mut guard = ContainerLockGuard::lock_all(&[&self.container_ref]);
170 guard
171 .remove_item(self.container_ref.container_id(), slot, MAX_BOOKS_PER_SLOT)
172 .unwrap_or_else(ItemStack::empty)
173 }
174
175 #[must_use]
177 pub fn item(&self, slot: usize) -> Option<ItemStack> {
178 let container = self.container.lock();
179 let item = container.items.get(slot)?;
180 Some(item.copy_with_count(item.count()))
181 }
182
183 #[must_use]
185 pub fn last_interacted_slot(&self) -> i32 {
186 self.container.lock().last_interacted_slot
187 }
188
189 pub fn apply_container_items(&self, items: Vec<ItemStack>) {
191 let mut container = self.container.lock();
192 container.items.fill_with(ItemStack::empty);
193 for (destination, item) in container.items.iter_mut().zip(items) {
194 *destination = item;
195 }
196 container.state_update_pending = false;
197 drop(container);
198 BlockEntity::set_changed(self);
199 }
200
201 pub fn collect_container_component(&self) -> io::Result<ItemContainerContents> {
203 let container = self.container.lock();
204 let Some(last_non_empty) = container.items.iter().rposition(|item| !item.is_empty()) else {
205 return Ok(ItemContainerContents::empty());
206 };
207
208 let mut items = Vec::with_capacity(last_non_empty + 1);
209 for item in &container.items[..=last_non_empty] {
210 if item.is_empty() {
211 items.push(None);
212 } else {
213 items.push(Some(ItemStackTemplate::from_stack(item)?));
214 }
215 }
216 ItemContainerContents::new(items)
217 }
218}
219
220impl BlockEntity for ChiseledBookShelfBlockEntity {
221 fn base(&self) -> &BlockEntityBase {
222 &self.base
223 }
224
225 fn pre_remove_side_effects(&self, pos: BlockPos, _state: BlockStateId) {
226 let items = {
227 let mut container = self.container.lock();
228 container.state_update_pending = false;
229 mem::replace(
230 &mut container.items,
231 (0..CHISELED_BOOKSHELF_SLOTS)
232 .map(|_| ItemStack::empty())
233 .collect(),
234 )
235 };
236 let Some(world) = self.get_level() else {
237 return;
238 };
239 for item in items {
240 world.drop_item_stack(pos, item);
241 }
242 }
243
244 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
245 let nbt: NbtCompoundView<'_, '_> = nbt.into();
246 let mut container = self.container.lock();
247 container.items.fill_with(ItemStack::empty);
248 container.state_update_pending = false;
249
250 if let Some(items) = nbt.list(ITEMS_NBT_KEY)
251 && let Some(compounds) = items.compounds()
252 {
253 for compound in compounds {
254 let Some(slot) = compound.byte(ITEM_SLOT_NBT_KEY) else {
255 continue;
256 };
257 let Ok(slot) = usize::try_from(slot) else {
258 continue;
259 };
260 if slot < CHISELED_BOOKSHELF_SLOTS
261 && let Some(item) = ItemStack::from_borrowed_compound(&compound)
262 {
263 container.items[slot] = item;
264 }
265 }
266 }
267 container.last_interacted_slot = nbt
268 .int(LAST_INTERACTED_SLOT_NBT_KEY)
269 .unwrap_or(DEFAULT_LAST_INTERACTED_SLOT);
270 }
271
272 fn save_additional(&self, nbt: &mut NbtCompound) {
273 let container = self.container.lock();
274 let mut items = Vec::new();
275 for (slot, item) in container.items.iter().enumerate() {
276 if item.is_empty() {
277 continue;
278 }
279 if let NbtTag::Compound(mut item_nbt) = item.copy_with_count(item.count()).to_nbt_tag()
280 {
281 item_nbt.insert(ITEM_SLOT_NBT_KEY, slot as i8);
282 items.push(item_nbt);
283 }
284 }
285 nbt.insert(ITEMS_NBT_KEY, NbtList::Compound(items));
286 nbt.insert(LAST_INTERACTED_SLOT_NBT_KEY, container.last_interacted_slot);
287 }
288
289 fn container_ref(&self) -> Option<ContainerRef> {
290 Some(self.container_ref.clone())
291 }
292}
293
294impl Container for ChiseledBookShelfContainer {
295 fn items(&self) -> &[ItemStack] {
296 &self.items
297 }
298
299 fn items_mut(&mut self) -> &mut [ItemStack] {
300 &mut self.items
301 }
302
303 fn set_item(&mut self, slot: usize, stack: ItemStack) {
304 if slot >= CHISELED_BOOKSHELF_SLOTS {
305 return;
306 }
307 if stack.is_empty() {
308 let _ = self.remove_item(slot, MAX_BOOKS_PER_SLOT);
309 return;
310 }
311 if !stack.item().has_tag(&ItemTag::BOOKSHELF_BOOKS) {
312 return;
313 }
314
315 self.items[slot] = stack;
316 self.last_interacted_slot = slot as i32;
317 self.state_update_pending = true;
318 }
319
320 fn remove_item(&mut self, slot: usize, _count: i32) -> ItemStack {
321 let Some(item) = self.items.get_mut(slot) else {
322 return ItemStack::empty();
323 };
324 let removed = mem::take(item);
325 if !removed.is_empty() {
326 self.last_interacted_slot = slot as i32;
327 self.state_update_pending = true;
328 }
329 removed
330 }
331
332 fn get_max_stack_size(&self) -> i32 {
333 MAX_BOOKS_PER_SLOT
334 }
335
336 fn set_changed(&mut self) {}
337
338 fn can_place_item(&self, slot: usize, stack: &ItemStack) -> bool {
339 let Some(stored_item) = self.items.get(slot) else {
340 return false;
341 };
342 stack.item().has_tag(&ItemTag::BOOKSHELF_BOOKS)
343 && (stored_item.is_empty()
344 || stored_item.count() < self.get_max_stack_size_for_item(stack))
345 }
346
347 fn can_take_item(&self, destination: &dyn Container, _slot: usize, stack: &ItemStack) -> bool {
348 destination.items().iter().any(|destination_item| {
349 destination_item.is_empty()
350 || (ItemStack::is_same_item_same_components(stack, destination_item)
351 && destination_item.count() + stack.count()
352 <= destination.get_max_stack_size_for_item(destination_item))
353 })
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use std::io::Cursor;
360
361 use simdnbt::borrow::read_compound as read_borrowed_compound;
362 use steel_registry::{init_vanilla_registry, vanilla_items};
363
364 use super::*;
365 use crate::inventory::container::SimpleContainer;
366
367 const TEST_POS: BlockPos = BlockPos::new(1, 2, 3);
368 const FIRST_SLOT_INDEX: usize = 0;
369 const LAST_SLOT_INDEX: usize = CHISELED_BOOKSHELF_SLOTS - 1;
370 const INTERACTION_TEST_SLOT: usize = 4;
371 const OVERSIZED_TEST_STACK_COUNT: i32 = MAX_BOOKS_PER_SLOT + 1;
372 const DESTINATION_SLOT_COUNT: usize = 1;
373 const DESTINATION_SLOT_INDEX: usize = 0;
374 const WRITABLE_BOOK_COMPONENT_SLOT: usize = 0;
375 const EMPTY_COMPONENT_SLOT: usize = 1;
376 const ENCHANTED_BOOK_COMPONENT_SLOT: usize = 2;
377 const FIRST_UNSPECIFIED_COMPONENT_SLOT: usize = 3;
378
379 fn test_bookshelf() -> ChiseledBookShelfBlockEntity {
380 init_vanilla_registry();
381 ChiseledBookShelfBlockEntity::new(
382 Weak::new(),
383 TEST_POS,
384 vanilla_blocks::CHISELED_BOOKSHELF.default_state(),
385 )
386 }
387
388 #[test]
389 fn bookshelf_book_tag_and_slot_capacity_control_container_capability() {
390 let bookshelf = test_bookshelf();
391 let valid_items = [
392 &vanilla_items::BOOK,
393 &vanilla_items::WRITABLE_BOOK,
394 &vanilla_items::WRITTEN_BOOK,
395 &vanilla_items::KNOWLEDGE_BOOK,
396 &vanilla_items::ENCHANTED_BOOK,
397 ];
398
399 for (slot, item) in valid_items.into_iter().enumerate() {
400 assert!(bookshelf.insert_book(slot, ItemStack::new(item)));
401 }
402 let invalid_item = ItemStack::new(&vanilla_items::STONE);
403 assert!(!bookshelf.insert_book(LAST_SLOT_INDEX, invalid_item));
404
405 let container = bookshelf.container.lock();
406 assert_eq!(container.get_max_stack_size(), MAX_BOOKS_PER_SLOT);
407 let valid_item = ItemStack::new(&vanilla_items::BOOK);
408 let invalid_item = ItemStack::new(&vanilla_items::STONE);
409 assert!(!container.can_place_item(FIRST_SLOT_INDEX, &valid_item));
410 assert!(!container.can_place_item(FIRST_SLOT_INDEX, &invalid_item));
411 assert!(container.can_place_item(LAST_SLOT_INDEX, &valid_item));
412 assert!(!container.can_place_item(LAST_SLOT_INDEX, &invalid_item));
413 assert!(!container.can_place_item(CHISELED_BOOKSHELF_SLOTS, &valid_item));
414 }
415
416 #[test]
417 fn insertion_and_removal_update_occupancy_and_last_slot_storage() {
418 let bookshelf = test_bookshelf();
419 assert_eq!(
420 bookshelf.last_interacted_slot(),
421 DEFAULT_LAST_INTERACTED_SLOT
422 );
423
424 assert!(bookshelf.insert_book(
425 INTERACTION_TEST_SLOT,
426 ItemStack::with_count(&vanilla_items::WRITTEN_BOOK, OVERSIZED_TEST_STACK_COUNT),
427 ));
428 assert_eq!(
429 bookshelf.last_interacted_slot(),
430 INTERACTION_TEST_SLOT as i32,
431 );
432 assert_eq!(
433 bookshelf
434 .item(INTERACTION_TEST_SLOT)
435 .map(|item| item.count()),
436 Some(OVERSIZED_TEST_STACK_COUNT),
437 );
438 let replacement = ItemStack::new(&vanilla_items::BOOK);
439 assert!(!bookshelf.insert_book(INTERACTION_TEST_SLOT, replacement));
440
441 let removed = bookshelf.remove_book(INTERACTION_TEST_SLOT);
442 assert!(removed.is(&vanilla_items::WRITTEN_BOOK));
443 assert_eq!(removed.count(), OVERSIZED_TEST_STACK_COUNT);
444 assert_eq!(
445 bookshelf.last_interacted_slot(),
446 INTERACTION_TEST_SLOT as i32,
447 );
448 assert!(
449 bookshelf
450 .item(INTERACTION_TEST_SLOT)
451 .is_some_and(|item| item.is_empty())
452 );
453 assert!(bookshelf.remove_book(INTERACTION_TEST_SLOT).is_empty());
454 }
455
456 #[test]
457 fn extraction_requires_space_somewhere_in_destination() {
458 let bookshelf = test_bookshelf();
459 let source = ItemStack::new(&vanilla_items::BOOK);
460 let container = bookshelf.container.lock();
461 let mut destination = SimpleContainer::new(DESTINATION_SLOT_COUNT);
462 let full_stack_size = source.max_stack_size();
463 let one_item_short_of_full_stack = full_stack_size - 1;
464
465 assert!(container.can_take_item(&destination, DESTINATION_SLOT_INDEX, &source));
466 destination.set_item(
467 DESTINATION_SLOT_INDEX,
468 ItemStack::new(&vanilla_items::STONE),
469 );
470 assert!(!container.can_take_item(&destination, DESTINATION_SLOT_INDEX, &source));
471 destination.set_item(
472 DESTINATION_SLOT_INDEX,
473 ItemStack::with_count(&vanilla_items::BOOK, one_item_short_of_full_stack),
474 );
475 assert!(container.can_take_item(&destination, DESTINATION_SLOT_INDEX, &source));
476 destination.set_item(
477 DESTINATION_SLOT_INDEX,
478 ItemStack::with_count(&vanilla_items::BOOK, full_stack_size),
479 );
480 assert!(!container.can_take_item(&destination, DESTINATION_SLOT_INDEX, &source));
481 }
482
483 #[test]
484 fn persistence_and_container_component_preserve_all_slots_and_last_slot() {
485 let bookshelf = test_bookshelf();
486 for slot in 0..LAST_SLOT_INDEX {
487 assert!(bookshelf.insert_book(slot, ItemStack::new(&vanilla_items::BOOK)));
488 }
489 assert!(bookshelf.insert_book(
490 LAST_SLOT_INDEX,
491 ItemStack::new(&vanilla_items::ENCHANTED_BOOK),
492 ));
493
494 let component = bookshelf
495 .collect_container_component()
496 .expect("valid bookshelf items should form a container component");
497 assert_eq!(component.items().len(), CHISELED_BOOKSHELF_SLOTS);
498 for item in &component.items()[..LAST_SLOT_INDEX] {
499 assert!(
500 item.as_ref()
501 .is_some_and(|item| item.item().key == vanilla_items::BOOK.key)
502 );
503 }
504 assert!(
505 component.items()[LAST_SLOT_INDEX]
506 .as_ref()
507 .is_some_and(|item| item.item().key == vanilla_items::ENCHANTED_BOOK.key)
508 );
509
510 let mut nbt = NbtCompound::new();
511 bookshelf.save_additional(&mut nbt);
512 let mut bytes = Vec::new();
513 nbt.write(&mut bytes);
514 let borrowed = read_borrowed_compound(&mut Cursor::new(bytes.as_slice()))
515 .expect("saved bookshelf NBT should decode");
516
517 let loaded = test_bookshelf();
518 loaded.load_additional(&borrowed);
519 assert_eq!(loaded.last_interacted_slot(), LAST_SLOT_INDEX as i32);
520 for slot in 0..LAST_SLOT_INDEX {
521 assert!(
522 loaded
523 .item(slot)
524 .is_some_and(|item| item.is(&vanilla_items::BOOK))
525 );
526 }
527 assert!(
528 loaded
529 .item(LAST_SLOT_INDEX)
530 .is_some_and(|item| item.is(&vanilla_items::ENCHANTED_BOOK))
531 );
532 }
533
534 #[test]
535 fn component_application_replaces_all_six_slots_without_changing_last_slot() {
536 let bookshelf = test_bookshelf();
537 let previously_inserted_book = ItemStack::new(&vanilla_items::BOOK);
538 assert!(bookshelf.insert_book(INTERACTION_TEST_SLOT, previously_inserted_book));
539
540 bookshelf.apply_container_items(vec![
541 ItemStack::new(&vanilla_items::WRITABLE_BOOK),
542 ItemStack::empty(),
543 ItemStack::new(&vanilla_items::ENCHANTED_BOOK),
544 ]);
545
546 assert_eq!(
547 bookshelf.last_interacted_slot(),
548 INTERACTION_TEST_SLOT as i32,
549 );
550 assert!(
551 bookshelf
552 .item(WRITABLE_BOOK_COMPONENT_SLOT)
553 .is_some_and(|item| item.is(&vanilla_items::WRITABLE_BOOK))
554 );
555 assert!(
556 bookshelf
557 .item(EMPTY_COMPONENT_SLOT)
558 .is_some_and(|item| item.is_empty())
559 );
560 assert!(
561 bookshelf
562 .item(ENCHANTED_BOOK_COMPONENT_SLOT)
563 .is_some_and(|item| item.is(&vanilla_items::ENCHANTED_BOOK))
564 );
565 for slot in FIRST_UNSPECIFIED_COMPONENT_SLOT..CHISELED_BOOKSHELF_SLOTS {
566 assert!(bookshelf.item(slot).is_some_and(|item| item.is_empty()));
567 }
568 }
569}