Skip to main content

steel_core/behavior/blocks/container/
chiseled_bookshelf_block.rs

1//! Vanilla chiseled bookshelf placement, interaction, and comparator behavior.
2
3use 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/// Direct-interaction behavior for Vanilla's chiseled bookshelf.
33///
34/// The static full-block shape and six occupied properties come from extracted
35/// block-state data. Structure rotation and mirroring use Steel's common facing
36/// property transform, matching `HorizontalDirectionalBlock`.
37#[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    /// Creates behavior for the chiseled bookshelf block.
55    #[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}