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