Skip to main content

steel_core/behavior/blocks/decoration/
flower_pot_block.rs

1//! `FlowerPotBlock` behavior (`net.minecraft.world.level.block.FlowerPotBlock`).
2//!
3//! TODO: Potted open/closed eyeblossoms do not transform at day/night yet.
4//! Vanilla reads `EnvironmentAttributes.EYEBLOSSOM_OPEN`, which Steel lacks
5//! (the same gap documented on `EyeblossomBlock`).
6
7use crate::player::player_inventory::Prediction;
8use std::sync::{Arc, LazyLock};
9
10use rustc_hash::FxHashMap;
11use steel_macros::block_behavior;
12use steel_registry::blocks::BlockRef;
13use steel_registry::item_stack::ItemStack;
14use steel_registry::items::item::BlockHitResult;
15use steel_registry::vanilla_custom_stats;
16use steel_registry::{REGISTRY, vanilla_blocks, vanilla_game_events};
17use steel_utils::locks::SyncMutex;
18use steel_utils::types::{InteractionHand, UpdateFlags};
19use steel_utils::{BlockPos, BlockStateId, Direction, Identifier};
20
21use crate::behavior::{BlockBehavior, BlockPlaceContext, InteractionResult, InventoryAccess};
22use crate::block_entity::SharedBlockEntity;
23use crate::entity::ai::path::PathComputationType;
24use crate::player::Player;
25use crate::world::game_event::GameEventContext;
26use crate::world::{ScheduledTickAccess, World};
27
28/// Maps a potted content block to the flower pot block that holds it.
29///
30/// Populated by [`FlowerPotBlock::new`]
31static POTTED_BY_CONTENT: LazyLock<SyncMutex<FxHashMap<Identifier, BlockRef>>> =
32    LazyLock::new(|| SyncMutex::new(FxHashMap::default()));
33
34/// Implements the `FlowerPotBlock` behaviour.
35#[block_behavior]
36pub struct FlowerPotBlock {
37    block: BlockRef,
38    /// The plant block inside the pot, or `air` for an empty pot.
39    #[json_arg(vanilla_blocks, json = "potted")]
40    potted: BlockRef,
41}
42
43impl FlowerPotBlock {
44    /// Creates flower-pot behavior and records the content → potted-block mapping.
45    #[must_use]
46    pub fn new(block: BlockRef, potted: BlockRef) -> Self {
47        if potted != &vanilla_blocks::AIR {
48            POTTED_BY_CONTENT.lock().insert(potted.key.clone(), block);
49        }
50        Self { block, potted }
51    }
52
53    fn is_empty(&self) -> bool {
54        self.potted == &vanilla_blocks::AIR
55    }
56
57    /// Returns the potted block holding `content`
58    fn potted_block_for(content: BlockRef) -> Option<BlockRef> {
59        POTTED_BY_CONTENT.lock().get(&content.key).copied()
60    }
61}
62
63impl BlockBehavior for FlowerPotBlock {
64    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
65        Some(self.block.default_state())
66    }
67
68    fn use_item_on(
69        &self,
70        _state: BlockStateId,
71        world: &Arc<World>,
72        pos: BlockPos,
73        player: &Player,
74        _hand: InteractionHand,
75        _hit_result: &BlockHitResult,
76        inv: &mut InventoryAccess,
77    ) -> InteractionResult {
78        let content = inv.with_item(|item| item.item().block);
79        let Some(new_contents) = content.and_then(Self::potted_block_for) else {
80            return InteractionResult::TryEmptyHandInteraction;
81        };
82
83        if !self.is_empty() {
84            return InteractionResult::Consume;
85        }
86
87        world.set_block(pos, new_contents.default_state(), UpdateFlags::UPDATE_ALL);
88        world.game_event(
89            &vanilla_game_events::BLOCK_CHANGE,
90            pos,
91            &GameEventContext::new(Some(player), None),
92        );
93        player.award_custom_stat(&vanilla_custom_stats::POT_FLOWER);
94        let has_infinite_materials = player.has_infinite_materials();
95        inv.with_item(|item| item.consume_one(has_infinite_materials));
96        InteractionResult::Success
97    }
98
99    fn use_without_item(
100        &self,
101        _state: BlockStateId,
102        world: &Arc<World>,
103        pos: BlockPos,
104        player: &Player,
105        _hit_result: &BlockHitResult,
106        _inv: &mut InventoryAccess,
107    ) -> InteractionResult {
108        if self.is_empty() {
109            return InteractionResult::Consume;
110        }
111
112        player.add_item_or_drop(
113            ItemStack::new(REGISTRY.items.by_block(self.potted)),
114            Prediction::Predicted,
115        );
116        world.set_block(
117            pos,
118            vanilla_blocks::FLOWER_POT.default_state(),
119            UpdateFlags::UPDATE_ALL,
120        );
121        world.game_event(
122            &vanilla_game_events::BLOCK_CHANGE,
123            pos,
124            &GameEventContext::new(Some(player), None),
125        );
126        InteractionResult::Success
127    }
128
129    fn get_clone_item_stack(
130        &self,
131        block: BlockRef,
132        _state: BlockStateId,
133        _block_entity: Option<SharedBlockEntity>,
134        _include_data: bool,
135    ) -> Option<ItemStack> {
136        let item = if self.is_empty() {
137            REGISTRY.items.by_block(block)
138        } else {
139            REGISTRY.items.by_block(self.potted)
140        };
141        Some(ItemStack::new(item))
142    }
143
144    fn update_shape(
145        &self,
146        state: BlockStateId,
147        world: &dyn ScheduledTickAccess,
148        pos: BlockPos,
149        direction: Direction,
150        _neighbor_pos: BlockPos,
151        _neighbor_state: BlockStateId,
152    ) -> BlockStateId {
153        if direction == Direction::Down && !self.can_survive(state, world, pos) {
154            vanilla_blocks::AIR.default_state()
155        } else {
156            state
157        }
158    }
159
160    fn is_pathfindable(
161        &self,
162        _state: BlockStateId,
163        _computation_type: PathComputationType,
164    ) -> bool {
165        false
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use glam::DVec3;
172    use steel_registry::blocks::block_state_ext::BlockStateExt as _;
173    use steel_registry::{init_vanilla_registry, vanilla_items};
174    use steel_utils::{ChunkPos, Direction};
175
176    use super::*;
177    use crate::bootstrap::init_globals;
178    use crate::test_support::{
179        TestLevel, TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk,
180    };
181
182    const TEST_POS: BlockPos = BlockPos::new(8, 64, 8);
183    const TEST_PLAYER_ENTITY_ID: i32 = 1;
184
185    fn empty_pot() -> FlowerPotBlock {
186        FlowerPotBlock::new(&vanilla_blocks::FLOWER_POT, &vanilla_blocks::AIR)
187    }
188
189    fn potted_dandelion() -> FlowerPotBlock {
190        FlowerPotBlock::new(
191            &vanilla_blocks::POTTED_DANDELION,
192            &vanilla_blocks::DANDELION,
193        )
194    }
195
196    fn interaction_hit() -> BlockHitResult {
197        BlockHitResult {
198            location: DVec3::new(8.5, 65.0, 8.5),
199            direction: Direction::Up,
200            block_pos: TEST_POS,
201            miss: false,
202            inside: false,
203            world_border_hit: false,
204        }
205    }
206
207    fn test_player(world: &Arc<World>) -> Arc<Player> {
208        TestPlayerBuilder::new(Arc::clone(world), "FlowerPotTester", TEST_PLAYER_ENTITY_ID).build()
209    }
210
211    #[test]
212    fn placing_and_removing_a_plant_swaps_the_potted_state() {
213        init_globals();
214        let world_fixture = fresh_test_world("flower_pot_interaction");
215        let world = &world_fixture.world;
216        insert_ready_full_chunk(world, ChunkPos::from_block_pos(TEST_POS));
217        let player = test_player(world);
218        let mut inventory =
219            InventoryAccess::new(Arc::clone(&player.inventory), InteractionHand::MainHand);
220
221        assert!(world.set_block(
222            TEST_POS,
223            vanilla_blocks::FLOWER_POT.default_state(),
224            UpdateFlags::UPDATE_ALL
225        ));
226
227        player
228            .inventory
229            .lock()
230            .set_selected_item(ItemStack::with_count(&vanilla_items::DANDELION, 1));
231        assert_eq!(
232            empty_pot().use_item_on(
233                world.get_block_state(TEST_POS),
234                world,
235                TEST_POS,
236                &player,
237                InteractionHand::MainHand,
238                &interaction_hit(),
239                &mut inventory,
240            ),
241            InteractionResult::Success,
242        );
243        assert_eq!(
244            world.get_block_state(TEST_POS).get_block(),
245            &vanilla_blocks::POTTED_DANDELION
246        );
247        assert!(player.inventory.lock().get_selected_item().is_empty());
248
249        player
250            .inventory
251            .lock()
252            .set_selected_item(ItemStack::new(&vanilla_items::STONE));
253        assert_eq!(
254            potted_dandelion().use_item_on(
255                world.get_block_state(TEST_POS),
256                world,
257                TEST_POS,
258                &player,
259                InteractionHand::MainHand,
260                &interaction_hit(),
261                &mut inventory,
262            ),
263            InteractionResult::TryEmptyHandInteraction,
264        );
265        assert_eq!(
266            potted_dandelion().use_without_item(
267                world.get_block_state(TEST_POS),
268                world,
269                TEST_POS,
270                &player,
271                &interaction_hit(),
272                &mut inventory,
273            ),
274            InteractionResult::Success,
275        );
276        assert_eq!(
277            world.get_block_state(TEST_POS).get_block(),
278            &vanilla_blocks::FLOWER_POT
279        );
280        assert!(
281            player
282                .inventory
283                .lock()
284                .get_items()
285                .iter()
286                .any(|item| item.is(&vanilla_items::DANDELION))
287        );
288
289        assert_eq!(
290            empty_pot().use_without_item(
291                world.get_block_state(TEST_POS),
292                world,
293                TEST_POS,
294                &player,
295                &interaction_hit(),
296                &mut inventory,
297            ),
298            InteractionResult::Consume,
299        );
300    }
301
302    #[test]
303    fn update_shape_leaves_a_floating_pot_intact() {
304        init_vanilla_registry();
305        let behavior = empty_pot();
306        let state = vanilla_blocks::FLOWER_POT.default_state();
307        let level = TestLevel::default();
308        assert_eq!(
309            behavior.update_shape(
310                state,
311                &level,
312                BlockPos::ZERO,
313                Direction::Down,
314                BlockPos::ZERO.below(),
315                vanilla_blocks::AIR.default_state(),
316            ),
317            state,
318        );
319    }
320
321    #[test]
322    fn clone_item_stack_returns_the_potted_plant() {
323        init_vanilla_registry();
324        let empty_clone = empty_pot()
325            .get_clone_item_stack(
326                &vanilla_blocks::FLOWER_POT,
327                vanilla_blocks::FLOWER_POT.default_state(),
328                None,
329                false,
330            )
331            .expect("flower pot always yields a clone stack");
332        assert!(empty_clone.is(&vanilla_items::FLOWER_POT));
333
334        let filled_clone = potted_dandelion()
335            .get_clone_item_stack(
336                &vanilla_blocks::POTTED_DANDELION,
337                vanilla_blocks::POTTED_DANDELION.default_state(),
338                None,
339                false,
340            )
341            .expect("potted flower pot always yields a clone stack");
342        assert!(filled_clone.is(&vanilla_items::DANDELION));
343    }
344}