Skip to main content

steel_core/behavior/blocks/container/
chiseled_bookshelf_block.rs

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