1use crate::player::player_inventory::Prediction;
4use std::sync::{Arc, Weak};
5
6use glam::DVec3;
7use steel_macros::block_behavior;
8use steel_registry::blocks::BlockRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::blocks::properties::{BlockStateProperties, Direction, EnumProperty};
11use steel_registry::data_components::vanilla_components::CONTAINER;
12use steel_registry::item_stack::ItemStack;
13use steel_registry::sound_event::SoundEventRef;
14use steel_registry::stat::vanilla_stat_types;
15use steel_registry::vanilla_item_tags::ItemTag;
16use steel_registry::{
17 sound_events, vanilla_block_entity_types, vanilla_game_events, vanilla_items,
18};
19use steel_utils::types::InteractionHand;
20use steel_utils::{BlockPos, BlockStateId, Downcast as _};
21
22use crate::behavior::{
23 BlockBehavior, BlockEntityCreation, BlockHitResult, BlockPlaceContext, InteractionResult,
24 InventoryAccess, PlacementSource,
25};
26use crate::block_entity::BLOCK_ENTITIES;
27use crate::block_entity::entities::{CHISELED_BOOKSHELF_SLOTS, ChiseledBookShelfBlockEntity};
28use crate::player::Player;
29use crate::world::World;
30use crate::world::game_event::GameEventContext;
31
32#[block_behavior]
38pub struct ChiseledBookShelfBlock {
39 block: BlockRef,
40}
41
42const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
43
44impl ChiseledBookShelfBlock {
45 const PIXELS_PER_BLOCK_EDGE: f32 = 16.0;
46 const BOOKS_PER_INTERACTION: i32 = 1;
47 const COLUMN_COUNT: i32 = 3;
48 const ROW_COUNT: i32 = 2;
49 const SOUND_VOLUME: f32 = 1.0;
50 const SOUND_PITCH: f32 = 1.0;
51 const NO_COMPARATOR_OUTPUT: i32 = 0;
52 const COMPARATOR_SLOT_INDEX_OFFSET: i32 = 1;
53
54 #[must_use]
56 pub const fn new(block: BlockRef) -> Self {
57 Self { block }
58 }
59
60 fn section_index(normalized_coordinate: f64, section_count: i32) -> usize {
61 let targeted_pixel = normalized_coordinate as f32 * Self::PIXELS_PER_BLOCK_EDGE;
62 let section_size = Self::PIXELS_PER_BLOCK_EDGE / section_count as f32;
63 ((targeted_pixel / section_size).floor() as i32).clamp(0, section_count - 1) as usize
64 }
65
66 fn hit_slot(state: BlockStateId, hit_result: &BlockHitResult) -> Option<usize> {
67 let facing = state.get_value(HORIZONTAL_FACING);
68 if hit_result.direction != facing {
69 return None;
70 }
71
72 let hit_face_origin = hit_result.direction.relative(hit_result.block_pos);
73 let relative_hit = hit_result.location
74 - DVec3::new(
75 f64::from(hit_face_origin.x()),
76 f64::from(hit_face_origin.y()),
77 f64::from(hit_face_origin.z()),
78 );
79 let horizontal_fraction = match facing {
80 Direction::North => 1.0 - relative_hit.x,
81 Direction::South => relative_hit.x,
82 Direction::West => relative_hit.z,
83 Direction::East => 1.0 - relative_hit.z,
84 Direction::Down | Direction::Up => return None,
85 };
86 let row = Self::section_index(1.0 - relative_hit.y, Self::ROW_COUNT);
87 let column = Self::section_index(horizontal_fraction, Self::COLUMN_COUNT);
88 Some(column + row * Self::COLUMN_COUNT as usize)
89 }
90
91 fn insert_sound(item: &ItemStack) -> SoundEventRef {
92 if item.is(&vanilla_items::ENCHANTED_BOOK) {
93 return &sound_events::BLOCK_CHISELED_BOOKSHELF_INSERT_ENCHANTED;
94 }
95 &sound_events::BLOCK_CHISELED_BOOKSHELF_INSERT
96 }
97
98 fn pickup_sound(item: &ItemStack) -> SoundEventRef {
99 if item.is(&vanilla_items::ENCHANTED_BOOK) {
100 return &sound_events::BLOCK_CHISELED_BOOKSHELF_PICKUP_ENCHANTED;
101 }
102 &sound_events::BLOCK_CHISELED_BOOKSHELF_PICKUP
103 }
104}
105
106impl BlockBehavior for ChiseledBookShelfBlock {
107 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
108 Some(
109 self.block
110 .default_state()
111 .set_value(HORIZONTAL_FACING, context.horizontal_direction().opposite()),
112 )
113 }
114
115 fn set_placed_by(
116 &self,
117 _state: BlockStateId,
118 world: &Arc<World>,
119 pos: BlockPos,
120 source: &PlacementSource<'_>,
121 ) {
122 let component_items = source.with_item(|item| {
123 item.get(CONTAINER).map(|contents| {
124 contents
125 .items()
126 .iter()
127 .take(CHISELED_BOOKSHELF_SLOTS)
128 .map(|item| {
129 item.as_ref().map_or_else(
130 ItemStack::empty,
131 steel_registry::ItemStackTemplate::create,
132 )
133 })
134 .collect::<Vec<_>>()
135 })
136 });
137 let Some(component_items) = component_items else {
138 return;
139 };
140 let Some(block_entity) = world.get_block_entity(pos) else {
141 return;
142 };
143 let Some(bookshelf) = block_entity.downcast_ref::<ChiseledBookShelfBlockEntity>() else {
144 return;
145 };
146 bookshelf.apply_container_items(component_items);
147 }
148
149 fn use_item_on(
150 &self,
151 state: BlockStateId,
152 world: &Arc<World>,
153 pos: BlockPos,
154 player: &Player,
155 _hand: InteractionHand,
156 hit_result: &BlockHitResult,
157 inv: &mut InventoryAccess,
158 ) -> InteractionResult {
159 let is_bookshelf_book = inv
160 .with_item(|item| !item.is_empty() && item.item().has_tag(&ItemTag::BOOKSHELF_BOOKS));
161 if !is_bookshelf_book {
162 return InteractionResult::TryEmptyHandInteraction;
163 }
164
165 let Some(slot) = Self::hit_slot(state, hit_result) else {
166 return InteractionResult::Pass;
167 };
168 let Some(block_entity) = world.get_block_entity(pos) else {
169 return InteractionResult::Pass;
170 };
171 let Some(bookshelf) = block_entity.downcast_ref::<ChiseledBookShelfBlockEntity>() else {
172 return InteractionResult::Pass;
173 };
174 if bookshelf.item(slot).is_some_and(|item| !item.is_empty()) {
175 return InteractionResult::TryEmptyHandInteraction;
176 }
177
178 let inserted = inv.with_item(|item| item.copy_with_count(Self::BOOKS_PER_INTERACTION));
179 let insert_sound = Self::insert_sound(&inserted);
180 let item = inserted.item;
181 if !bookshelf.insert_book(slot, inserted) {
182 return InteractionResult::Pass;
183 }
184 let has_infinite_materials = player.has_infinite_materials();
185 inv.with_item(|item| item.consume(Self::BOOKS_PER_INTERACTION, has_infinite_materials));
186 world.play_block_sound(
187 insert_sound,
188 pos,
189 Self::SOUND_VOLUME,
190 Self::SOUND_PITCH,
191 None,
192 );
193 player.award_stat(&vanilla_stat_types::ITEM_USED, item);
194 InteractionResult::Success
195 }
196
197 fn use_without_item(
198 &self,
199 state: BlockStateId,
200 world: &Arc<World>,
201 pos: BlockPos,
202 player: &Player,
203 hit_result: &BlockHitResult,
204 _inv: &mut InventoryAccess,
205 ) -> InteractionResult {
206 let Some(slot) = Self::hit_slot(state, hit_result) else {
207 return InteractionResult::Pass;
208 };
209 let Some(block_entity) = world.get_block_entity(pos) else {
210 return InteractionResult::Pass;
211 };
212 let Some(bookshelf) = block_entity.downcast_ref::<ChiseledBookShelfBlockEntity>() else {
213 return InteractionResult::Pass;
214 };
215 if bookshelf.item(slot).is_none_or(|item| item.is_empty()) {
216 return InteractionResult::Consume;
217 }
218
219 let removed = bookshelf.remove_book(slot);
220 if removed.is_empty() {
221 return InteractionResult::Consume;
222 }
223 world.play_block_sound(
224 Self::pickup_sound(&removed),
225 pos,
226 Self::SOUND_VOLUME,
227 Self::SOUND_PITCH,
228 None,
229 );
230 player.add_item_or_drop(removed, Prediction::ServerOnly);
231 world.game_event(
232 &vanilla_game_events::BLOCK_CHANGE,
233 pos,
234 &GameEventContext::new(Some(player), None),
235 );
236 InteractionResult::Success
237 }
238
239 fn affect_neighbors_after_removal(
240 &self,
241 state: BlockStateId,
242 world: &Arc<World>,
243 pos: BlockPos,
244 _moved_by_piston: bool,
245 ) {
246 world.update_neighbor_for_output_signal(pos, state.get_block());
247 }
248
249 fn new_block_entity(
250 &self,
251 level: Weak<World>,
252 pos: BlockPos,
253 state: BlockStateId,
254 ) -> BlockEntityCreation {
255 BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
256 &vanilla_block_entity_types::CHISELED_BOOKSHELF,
257 level,
258 pos,
259 state,
260 ))
261 }
262
263 fn has_analog_output_signal(&self, _state: BlockStateId) -> bool {
264 true
265 }
266
267 fn get_analog_output_signal(
268 &self,
269 _state: BlockStateId,
270 world: &World,
271 pos: BlockPos,
272 _direction: Direction,
273 ) -> i32 {
274 world
275 .get_block_entity(pos)
276 .and_then(|block_entity| {
277 block_entity
278 .downcast_ref::<ChiseledBookShelfBlockEntity>()
279 .map(ChiseledBookShelfBlockEntity::last_interacted_slot)
280 })
281 .map_or(Self::NO_COMPARATOR_OUTPUT, |slot| {
282 slot + Self::COMPARATOR_SLOT_INDEX_OFFSET
283 })
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use std::sync::Arc;
290
291 use steel_registry::ItemStackTemplate;
292 use steel_registry::blocks::shapes::is_shape_full_block;
293 use steel_registry::data_components::components::ItemContainerContents;
294 use steel_registry::{vanilla_blocks, vanilla_entities};
295 use steel_utils::types::{GameType, UpdateFlags};
296 use steel_utils::{ChunkPos, WorldAabb};
297 use uuid::Uuid;
298
299 use super::*;
300 use crate::behavior::PlacementOrientation;
301 use crate::bootstrap::init_globals;
302 use crate::entity::entities::ItemEntity;
303 use crate::inventory::container::Container as _;
304 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
305
306 const TEST_POS: BlockPos = BlockPos::new(8, 64, 8);
307 const TEST_PLAYER_UUID: Uuid = Uuid::from_u128(1);
308 const TEST_PLAYER_ENTITY_ID: i32 = 1;
309 const INTERACTION_FACING: Direction = Direction::South;
310 const COMPONENT_PLACER_FACING: Direction = Direction::South;
311 const ARBITRARY_COMPARATOR_QUERY_DIRECTION: Direction = Direction::North;
312 const BLOCK_EDGE_IN_PIXELS: f64 = ChiseledBookShelfBlock::PIXELS_PER_BLOCK_EDGE as f64;
313 const LEFT_COLUMN_HIT: f64 = 2.5 / BLOCK_EDGE_IN_PIXELS;
314 const MIDDLE_COLUMN_HIT: f64 = 7.5 / BLOCK_EDGE_IN_PIXELS;
315 const RIGHT_COLUMN_HIT: f64 = 13.0 / BLOCK_EDGE_IN_PIXELS;
316 const TOP_ROW_HIT: f64 = 12.0 / BLOCK_EDGE_IN_PIXELS;
317 const BOTTOM_ROW_HIT: f64 = 4.0 / BLOCK_EDGE_IN_PIXELS;
318 const FACE_CENTER_HIT: f64 = 8.0 / BLOCK_EDGE_IN_PIXELS;
319 const FIRST_COLUMN_BOUNDARY: f64 = 1.0 / 3.0;
320 const SECOND_COLUMN_BOUNDARY: f64 = 2.0 / 3.0;
321 const ROW_BOUNDARY: f64 = FACE_CENTER_HIT;
322 const BOUNDARY_EPSILON: f64 = 1.0e-6;
323 const JUST_BEFORE_FIRST_COLUMN_BOUNDARY: f64 = FIRST_COLUMN_BOUNDARY - BOUNDARY_EPSILON;
324 const JUST_BEFORE_SECOND_COLUMN_BOUNDARY: f64 = SECOND_COLUMN_BOUNDARY - BOUNDARY_EPSILON;
325 const JUST_ABOVE_ROW_BOUNDARY: f64 = ROW_BOUNDARY + BOUNDARY_EPSILON;
326 const TOP_LEFT_SLOT: usize = 0;
327 const TOP_MIDDLE_SLOT: usize = 1;
328 const TOP_RIGHT_SLOT: usize = 2;
329 const BOTTOM_LEFT_SLOT: usize = 3;
330 const BOTTOM_RIGHT_SLOT: usize = CHISELED_BOOKSHELF_SLOTS - 1;
331 const DROP_SEARCH_MARGIN: f64 = 1.0;
332 const SURVIVAL_STARTING_BOOK_COUNT: i32 = ChiseledBookShelfBlock::BOOKS_PER_INTERACTION + 1;
333 const EXPECTED_ENCHANTED_BOOK_DROP_COUNT: i32 = ChiseledBookShelfBlock::BOOKS_PER_INTERACTION;
334
335 fn state_facing(facing: Direction) -> BlockStateId {
336 vanilla_blocks::CHISELED_BOOKSHELF
337 .default_state()
338 .set_value(HORIZONTAL_FACING, facing)
339 }
340
341 fn bookshelf_hit(
342 facing: Direction,
343 horizontal_fraction: f64,
344 height_fraction: f64,
345 ) -> BlockHitResult {
346 let x = f64::from(TEST_POS.x());
347 let block_y = f64::from(TEST_POS.y());
348 let z = f64::from(TEST_POS.z());
349 let location = match facing {
350 Direction::North => {
351 DVec3::new(x + 1.0 - horizontal_fraction, block_y + height_fraction, z)
352 }
353 Direction::South => {
354 DVec3::new(x + horizontal_fraction, block_y + height_fraction, z + 1.0)
355 }
356 Direction::West => DVec3::new(x, block_y + height_fraction, z + horizontal_fraction),
357 Direction::East => DVec3::new(
358 x + 1.0,
359 block_y + height_fraction,
360 z + 1.0 - horizontal_fraction,
361 ),
362 Direction::Down | Direction::Up => unreachable!("test uses horizontal facings"),
363 };
364 BlockHitResult {
365 location,
366 direction: facing,
367 block_pos: TEST_POS,
368 miss: false,
369 inside: false,
370 world_border_hit: false,
371 }
372 }
373
374 fn test_player(world: &Arc<World>) -> Arc<Player> {
375 TestPlayerBuilder::new(Arc::clone(world), "BookshelfTester", TEST_PLAYER_ENTITY_ID)
376 .uuid(TEST_PLAYER_UUID)
377 .build()
378 }
379
380 fn assert_hit_targets_slot(
381 state: BlockStateId,
382 hit_result: &BlockHitResult,
383 expected_slot: usize,
384 ) {
385 assert_eq!(
386 ChiseledBookShelfBlock::hit_slot(state, hit_result),
387 Some(expected_slot),
388 );
389 }
390
391 fn expected_comparator_output(slot: usize) -> i32 {
392 slot as i32 + ChiseledBookShelfBlock::COMPARATOR_SLOT_INDEX_OFFSET
393 }
394
395 #[test]
396 fn all_six_hit_regions_map_identically_for_every_horizontal_facing() {
397 init_globals();
398 let column_hits = [LEFT_COLUMN_HIT, MIDDLE_COLUMN_HIT, RIGHT_COLUMN_HIT];
399 for facing in Direction::HORIZONTAL {
400 let state = state_facing(facing);
401 for (column, horizontal_fraction) in column_hits.into_iter().enumerate() {
402 let top_slot = column;
403 let bottom_slot = column + ChiseledBookShelfBlock::COLUMN_COUNT as usize;
404 assert_hit_targets_slot(
405 state,
406 &bookshelf_hit(facing, horizontal_fraction, TOP_ROW_HIT),
407 top_slot,
408 );
409 assert_hit_targets_slot(
410 state,
411 &bookshelf_hit(facing, horizontal_fraction, BOTTOM_ROW_HIT),
412 bottom_slot,
413 );
414 }
415
416 let mut wrong_face = bookshelf_hit(facing, FACE_CENTER_HIT, FACE_CENTER_HIT);
417 wrong_face.direction = facing.opposite();
418 assert_eq!(ChiseledBookShelfBlock::hit_slot(state, &wrong_face), None);
419 }
420 }
421
422 #[test]
423 fn hit_boundaries_use_vanilla_pixel_sections() {
424 init_globals();
425 let facing = INTERACTION_FACING;
426 let state = state_facing(facing);
427
428 assert_hit_targets_slot(
429 state,
430 &bookshelf_hit(facing, JUST_BEFORE_FIRST_COLUMN_BOUNDARY, TOP_ROW_HIT),
431 TOP_LEFT_SLOT,
432 );
433 assert_hit_targets_slot(
434 state,
435 &bookshelf_hit(facing, FIRST_COLUMN_BOUNDARY, TOP_ROW_HIT),
436 TOP_MIDDLE_SLOT,
437 );
438 assert_hit_targets_slot(
439 state,
440 &bookshelf_hit(facing, JUST_BEFORE_SECOND_COLUMN_BOUNDARY, TOP_ROW_HIT),
441 TOP_MIDDLE_SLOT,
442 );
443 assert_hit_targets_slot(
444 state,
445 &bookshelf_hit(facing, SECOND_COLUMN_BOUNDARY, TOP_ROW_HIT),
446 TOP_RIGHT_SLOT,
447 );
448 assert_hit_targets_slot(
449 state,
450 &bookshelf_hit(facing, LEFT_COLUMN_HIT, JUST_ABOVE_ROW_BOUNDARY),
451 TOP_LEFT_SLOT,
452 );
453 assert_hit_targets_slot(
454 state,
455 &bookshelf_hit(facing, LEFT_COLUMN_HIT, ROW_BOUNDARY),
456 BOTTOM_LEFT_SLOT,
457 );
458 }
459
460 #[test]
461 fn placement_faces_the_player_and_uses_the_extracted_full_block_shape() {
462 init_globals();
463 let world_fixture = fresh_test_world("chiseled_bookshelf_placement");
464 let world = &world_fixture.world;
465 let behavior = ChiseledBookShelfBlock::new(&vanilla_blocks::CHISELED_BOOKSHELF);
466
467 for facing in Direction::HORIZONTAL {
468 let mut stack = ItemStack::new(&vanilla_items::CHISELED_BOOKSHELF);
469 let is_secondary_use_active = false;
470 let source = PlacementSource::direct(
471 None,
472 InteractionHand::MainHand,
473 &mut stack,
474 PlacementOrientation::Directional { direction: facing },
475 is_secondary_use_active,
476 );
477 let context = BlockPlaceContext::new(
478 world,
479 source,
480 &bookshelf_hit(facing, FACE_CENTER_HIT, FACE_CENTER_HIT),
481 );
482 let placed = behavior
483 .get_state_for_placement(&context)
484 .expect("chiseled bookshelf always has a placement state");
485 assert_eq!(placed.get_value(HORIZONTAL_FACING), facing.opposite());
486 assert!(is_shape_full_block(placed.get_static_collision_shape()));
487 }
488 }
489
490 fn assert_invalid_item_is_rejected(
491 behavior: &ChiseledBookShelfBlock,
492 world: &Arc<World>,
493 state: BlockStateId,
494 player: &Player,
495 inventory: &mut InventoryAccess,
496 ) {
497 let facing = state.get_value(HORIZONTAL_FACING);
498 player
499 .inventory
500 .lock()
501 .set_selected_item(ItemStack::new(&vanilla_items::STONE));
502 assert_eq!(
503 behavior.use_item_on(
504 state,
505 world,
506 TEST_POS,
507 player,
508 InteractionHand::MainHand,
509 &bookshelf_hit(facing, LEFT_COLUMN_HIT, TOP_ROW_HIT),
510 inventory,
511 ),
512 InteractionResult::TryEmptyHandInteraction,
513 );
514 assert!(
515 player
516 .inventory
517 .lock()
518 .get_selected_item()
519 .is(&vanilla_items::STONE)
520 );
521 }
522
523 fn assert_top_left_slot_removal_returns_book(
524 behavior: &ChiseledBookShelfBlock,
525 world: &Arc<World>,
526 player: &Player,
527 inventory: &mut InventoryAccess,
528 ) {
529 let state = world.get_block_state(TEST_POS);
530 let facing = state.get_value(HORIZONTAL_FACING);
531 player
532 .inventory
533 .lock()
534 .set_selected_item(ItemStack::empty());
535 assert_eq!(
536 behavior.use_without_item(
537 state,
538 world,
539 TEST_POS,
540 player,
541 &bookshelf_hit(facing, LEFT_COLUMN_HIT, TOP_ROW_HIT),
542 inventory,
543 ),
544 InteractionResult::Success,
545 );
546 assert!(
547 !world
548 .get_block_state(TEST_POS)
549 .get_value(&BlockStateProperties::SLOT_0_OCCUPIED)
550 );
551 assert_eq!(
552 behavior.get_analog_output_signal(
553 world.get_block_state(TEST_POS),
554 world,
555 TEST_POS,
556 ARBITRARY_COMPARATOR_QUERY_DIRECTION,
557 ),
558 expected_comparator_output(TOP_LEFT_SLOT),
559 );
560 assert!(
561 player
562 .inventory
563 .lock()
564 .items()
565 .iter()
566 .any(|item| item.is(&vanilla_items::BOOK))
567 );
568 }
569
570 #[test]
571 fn interactions_update_inventory_occupied_state_and_comparator() {
572 init_globals();
573 let world_fixture = fresh_test_world("chiseled_bookshelf_interactions");
574 let world = &world_fixture.world;
575 let holder = insert_ready_full_chunk(world, ChunkPos::from_block_pos(TEST_POS));
576 let state = state_facing(INTERACTION_FACING);
577 assert!(world.set_block(TEST_POS, state, UpdateFlags::UPDATE_ALL));
578 let behavior = ChiseledBookShelfBlock::new(&vanilla_blocks::CHISELED_BOOKSHELF);
579 let player = test_player(world);
580 let mut inventory =
581 InventoryAccess::new(Arc::clone(&player.inventory), InteractionHand::MainHand);
582
583 assert_invalid_item_is_rejected(&behavior, world, state, &player, &mut inventory);
584
585 let revision = holder.packet_content_revision();
586 player
587 .inventory
588 .lock()
589 .set_selected_item(ItemStack::with_count(
590 &vanilla_items::BOOK,
591 SURVIVAL_STARTING_BOOK_COUNT,
592 ));
593 assert_eq!(
594 behavior.use_item_on(
595 state,
596 world,
597 TEST_POS,
598 &player,
599 InteractionHand::MainHand,
600 &bookshelf_hit(INTERACTION_FACING, LEFT_COLUMN_HIT, TOP_ROW_HIT),
601 &mut inventory,
602 ),
603 InteractionResult::Success,
604 );
605 assert_eq!(
606 player.inventory.lock().get_selected_item().count(),
607 SURVIVAL_STARTING_BOOK_COUNT - ChiseledBookShelfBlock::BOOKS_PER_INTERACTION,
608 );
609 assert!(
610 world
611 .get_block_state(TEST_POS)
612 .get_value(&BlockStateProperties::SLOT_0_OCCUPIED)
613 );
614 assert!(holder.packet_content_revision() > revision);
615 assert_eq!(
616 behavior.get_analog_output_signal(
617 world.get_block_state(TEST_POS),
618 world.as_ref(),
619 TEST_POS,
620 ARBITRARY_COMPARATOR_QUERY_DIRECTION,
621 ),
622 expected_comparator_output(TOP_LEFT_SLOT),
623 );
624
625 player.restore_game_modes(GameType::Creative, None);
626 player
627 .inventory
628 .lock()
629 .set_selected_item(ItemStack::new(&vanilla_items::ENCHANTED_BOOK));
630 let creative_book_count = player.inventory.lock().get_selected_item().count();
631 assert_eq!(
632 behavior.use_item_on(
633 world.get_block_state(TEST_POS),
634 world,
635 TEST_POS,
636 &player,
637 InteractionHand::MainHand,
638 &bookshelf_hit(INTERACTION_FACING, MIDDLE_COLUMN_HIT, TOP_ROW_HIT),
639 &mut inventory,
640 ),
641 InteractionResult::Success,
642 );
643 assert_eq!(
644 player.inventory.lock().get_selected_item().count(),
645 creative_book_count,
646 );
647
648 let block_entity = world
649 .get_block_entity(TEST_POS)
650 .expect("placed chiseled bookshelf should have a block entity");
651 let bookshelf = block_entity
652 .downcast_ref::<ChiseledBookShelfBlockEntity>()
653 .expect("registered chiseled bookshelf should use its concrete block entity");
654 let occupied_properties = [
655 &BlockStateProperties::SLOT_0_OCCUPIED,
656 &BlockStateProperties::SLOT_1_OCCUPIED,
657 &BlockStateProperties::SLOT_2_OCCUPIED,
658 &BlockStateProperties::SLOT_3_OCCUPIED,
659 &BlockStateProperties::SLOT_4_OCCUPIED,
660 &BlockStateProperties::SLOT_5_OCCUPIED,
661 ];
662 for (slot, property) in occupied_properties.iter().enumerate().skip(TOP_RIGHT_SLOT) {
663 assert!(bookshelf.insert_book(slot, ItemStack::new(&vanilla_items::BOOK)));
664 assert!(world.get_block_state(TEST_POS).get_value(*property));
665 assert_eq!(
666 behavior.get_analog_output_signal(
667 world.get_block_state(TEST_POS),
668 world.as_ref(),
669 TEST_POS,
670 ARBITRARY_COMPARATOR_QUERY_DIRECTION,
671 ),
672 expected_comparator_output(slot),
673 );
674 }
675
676 assert_top_left_slot_removal_returns_book(&behavior, world, &player, &mut inventory);
677 }
678
679 #[test]
680 fn enchanted_books_select_the_enchanted_insert_and_pickup_sounds() {
681 init_globals();
682 assert_eq!(
683 ChiseledBookShelfBlock::insert_sound(&ItemStack::new(&vanilla_items::BOOK)).key,
684 sound_events::BLOCK_CHISELED_BOOKSHELF_INSERT.key,
685 );
686 assert_eq!(
687 ChiseledBookShelfBlock::insert_sound(&ItemStack::new(&vanilla_items::ENCHANTED_BOOK))
688 .key,
689 sound_events::BLOCK_CHISELED_BOOKSHELF_INSERT_ENCHANTED.key,
690 );
691 assert_eq!(
692 ChiseledBookShelfBlock::pickup_sound(&ItemStack::new(&vanilla_items::BOOK)).key,
693 sound_events::BLOCK_CHISELED_BOOKSHELF_PICKUP.key,
694 );
695 assert_eq!(
696 ChiseledBookShelfBlock::pickup_sound(&ItemStack::new(&vanilla_items::ENCHANTED_BOOK))
697 .key,
698 sound_events::BLOCK_CHISELED_BOOKSHELF_PICKUP_ENCHANTED.key,
699 );
700 }
701
702 #[test]
703 fn placement_applies_the_container_component_to_the_block_entity() {
704 init_globals();
705 let world_fixture = fresh_test_world("chiseled_bookshelf_component_placement");
706 let world = &world_fixture.world;
707 insert_ready_full_chunk(world, ChunkPos::from_block_pos(TEST_POS));
708 let state = state_facing(COMPONENT_PLACER_FACING.opposite());
709 assert!(world.set_block(TEST_POS, state, UpdateFlags::UPDATE_ALL));
710 let behavior = ChiseledBookShelfBlock::new(&vanilla_blocks::CHISELED_BOOKSHELF);
711
712 let contents = ItemContainerContents::new(vec![
713 Some(ItemStackTemplate::new(&vanilla_items::BOOK)),
714 None,
715 Some(ItemStackTemplate::new(&vanilla_items::ENCHANTED_BOOK)),
716 ])
717 .expect("three component slots are valid");
718 let mut placed_item = ItemStack::new(&vanilla_items::CHISELED_BOOKSHELF);
719 placed_item.set(CONTAINER, contents);
720 let initial_last_interacted_slot = world
721 .get_block_entity(TEST_POS)
722 .and_then(|block_entity| {
723 block_entity
724 .downcast_ref::<ChiseledBookShelfBlockEntity>()
725 .map(ChiseledBookShelfBlockEntity::last_interacted_slot)
726 })
727 .expect("placed chiseled bookshelf should have its concrete block entity");
728 let is_secondary_use_active = false;
729 let source = PlacementSource::direct(
730 None,
731 InteractionHand::MainHand,
732 &mut placed_item,
733 PlacementOrientation::Directional {
734 direction: COMPONENT_PLACER_FACING,
735 },
736 is_secondary_use_active,
737 );
738 behavior.set_placed_by(state, world, TEST_POS, &source);
739
740 let block_entity = world
741 .get_block_entity(TEST_POS)
742 .expect("placed chiseled bookshelf should have a block entity");
743 let bookshelf = block_entity
744 .downcast_ref::<ChiseledBookShelfBlockEntity>()
745 .expect("registered chiseled bookshelf should use its concrete block entity");
746 assert!(
747 bookshelf
748 .item(TOP_LEFT_SLOT)
749 .is_some_and(|item| item.is(&vanilla_items::BOOK))
750 );
751 assert!(
752 bookshelf
753 .item(TOP_MIDDLE_SLOT)
754 .is_some_and(|item| item.is_empty())
755 );
756 assert!(
757 bookshelf
758 .item(TOP_RIGHT_SLOT)
759 .is_some_and(|item| item.is(&vanilla_items::ENCHANTED_BOOK))
760 );
761 assert_eq!(
762 bookshelf.last_interacted_slot(),
763 initial_last_interacted_slot,
764 );
765 }
766
767 #[test]
768 fn destruction_drains_and_drops_every_stored_book() {
769 init_globals();
770 let world_fixture = fresh_test_world("chiseled_bookshelf_drops");
771 let world = &world_fixture.world;
772 insert_ready_full_chunk(world, ChunkPos::from_block_pos(TEST_POS));
773 let state = state_facing(INTERACTION_FACING);
774 assert!(world.set_block(TEST_POS, state, UpdateFlags::UPDATE_ALL));
775 let block_entity = world
776 .get_block_entity(TEST_POS)
777 .expect("placed chiseled bookshelf should have a block entity");
778 let bookshelf = block_entity
779 .downcast_ref::<ChiseledBookShelfBlockEntity>()
780 .expect("registered chiseled bookshelf should use its concrete block entity");
781 for slot in 0..BOTTOM_RIGHT_SLOT {
782 assert!(bookshelf.insert_book(slot, ItemStack::new(&vanilla_items::BOOK)));
783 }
784 assert!(bookshelf.insert_book(
785 BOTTOM_RIGHT_SLOT,
786 ItemStack::new(&vanilla_items::ENCHANTED_BOOK),
787 ));
788
789 assert!(world.set_block(
790 TEST_POS,
791 vanilla_blocks::AIR.default_state(),
792 UpdateFlags::UPDATE_ALL,
793 ));
794 assert!(world.get_block_entity(TEST_POS).is_none());
795 for slot in 0..CHISELED_BOOKSHELF_SLOTS {
796 assert!(bookshelf.item(slot).is_some_and(|item| item.is_empty()));
797 }
798
799 let block_min = DVec3::new(
800 f64::from(TEST_POS.x()),
801 f64::from(TEST_POS.y()),
802 f64::from(TEST_POS.z()),
803 );
804 let drop_search_area =
805 WorldAabb::from_min_max(block_min, block_min + DVec3::ONE).inflate(DROP_SEARCH_MARGIN);
806 let drops = world.get_entities_in_aabb_matching(&drop_search_area, |entity| {
807 entity.entity_type() == &vanilla_entities::ITEM
808 });
809 assert_eq!(drops.len(), CHISELED_BOOKSHELF_SLOTS);
810 let dropped_items = drops
811 .iter()
812 .filter_map(|entity| {
813 entity
814 .downcast_ref::<ItemEntity>()
815 .map(ItemEntity::get_item)
816 })
817 .collect::<Vec<_>>();
818 assert_eq!(dropped_items.len(), CHISELED_BOOKSHELF_SLOTS);
819 assert_eq!(
820 dropped_items
821 .iter()
822 .filter(|item| item.is(&vanilla_items::BOOK))
823 .map(ItemStack::count)
824 .sum::<i32>(),
825 BOTTOM_RIGHT_SLOT as i32,
826 );
827 assert_eq!(
828 dropped_items
829 .iter()
830 .filter(|item| item.is(&vanilla_items::ENCHANTED_BOOK))
831 .map(ItemStack::count)
832 .sum::<i32>(),
833 EXPECTED_ENCHANTED_BOOK_DROP_COUNT,
834 );
835 }
836}