Skip to main content

steel_core/behavior/blocks/fluid/
bubble_column_block.rs

1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::blocks::BlockRef;
5use steel_registry::blocks::block_state_ext::BlockStateExt as _;
6use steel_registry::blocks::properties::{BlockStateProperties, Direction};
7use steel_registry::item_stack::ItemStack;
8use steel_registry::sound_events;
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_registry::vanilla_blocks;
11use steel_registry::vanilla_fluid_tags::FluidTag;
12use steel_registry::vanilla_fluids;
13use steel_registry::vanilla_items;
14use steel_utils::types::UpdateFlags;
15use steel_utils::{BlockPos, BlockStateId};
16
17use crate::behavior::context::BlockPlaceContext;
18use crate::behavior::{
19    BLOCK_BEHAVIORS, BlockCollisionContext, block::BlockBehavior, block::PickupResult,
20};
21use crate::entity::{Entity, InsideBlockEffectCollector};
22use crate::player::Player;
23use crate::world::{
24    ConditionalBlockSetResult, LevelAccessor, LevelReader, ScheduledTickAccess, World,
25};
26
27/// Vanilla `BubbleColumnBlock` column propagation and fluid state.
28#[block_behavior]
29pub struct BubbleColumnBlock {
30    block: BlockRef,
31}
32
33impl BubbleColumnBlock {
34    /// Creates a bubble column block behavior.
35    #[must_use]
36    pub const fn new(block: BlockRef) -> Self {
37        Self { block }
38    }
39
40    pub(super) fn update_column(
41        bubble_column: BlockRef,
42        level: &dyn LevelAccessor,
43        occupy_at: BlockPos,
44        below_state: BlockStateId,
45    ) {
46        Self::update_column_with_state(
47            bubble_column,
48            level,
49            occupy_at,
50            level.get_block_state(occupy_at),
51            below_state,
52        );
53    }
54
55    fn update_column_with_state(
56        bubble_column: BlockRef,
57        level: &dyn LevelAccessor,
58        occupy_at: BlockPos,
59        occupy_state: BlockStateId,
60        below_state: BlockStateId,
61    ) {
62        if !Self::can_occupy(bubble_column, occupy_state) {
63            return;
64        }
65
66        let column_state = Self::column_state(bubble_column, below_state, occupy_state);
67        level.set_block_state(occupy_at, column_state, UpdateFlags::UPDATE_CLIENTS);
68
69        let mut pos = occupy_at.above();
70        while Self::can_occupy(bubble_column, level.get_block_state(pos)) {
71            if !level.set_block_state(pos, column_state, UpdateFlags::UPDATE_CLIENTS) {
72                return;
73            }
74            pos = pos.above();
75        }
76    }
77
78    pub(super) fn can_occupy(bubble_column: BlockRef, occupy_state: BlockStateId) -> bool {
79        if occupy_state.get_block() == bubble_column {
80            return true;
81        }
82
83        let fluid_state = occupy_state.get_fluid_state();
84        fluid_state
85            .fluid_id
86            .has_tag(&FluidTag::BUBBLE_COLUMN_CAN_OCCUPY)
87            && occupy_state.get_block() == &vanilla_blocks::WATER
88            && fluid_state.is_source()
89            && fluid_state.amount >= 8
90    }
91
92    fn column_state(
93        bubble_column: BlockRef,
94        below_state: BlockStateId,
95        occupy_state: BlockStateId,
96    ) -> BlockStateId {
97        if below_state.get_block() == bubble_column {
98            return below_state;
99        }
100        if below_state
101            .get_block()
102            .has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_PUSH_UP)
103        {
104            return bubble_column
105                .default_state()
106                .set_value(&BlockStateProperties::DRAG, false);
107        }
108        if below_state
109            .get_block()
110            .has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_DRAG_DOWN)
111        {
112            return bubble_column
113                .default_state()
114                .set_value(&BlockStateProperties::DRAG, true);
115        }
116
117        if occupy_state.get_block() == bubble_column {
118            vanilla_blocks::WATER.default_state()
119        } else {
120            occupy_state
121        }
122    }
123
124    fn is_open_above(level: &dyn LevelReader, pos: BlockPos) -> bool {
125        let above_pos = pos.above();
126        let above_state = level.get_block_state(above_pos);
127        let behavior = BLOCK_BEHAVIORS.get_behavior(above_state.get_block());
128        behavior
129            .get_collision_shape(above_state, level, pos, BlockCollisionContext::empty())
130            .is_empty()
131            && above_state.get_fluid_state().is_empty()
132    }
133
134    fn apply_entity_effect(
135        state: BlockStateId,
136        level: &dyn LevelReader,
137        pos: BlockPos,
138        entity: &dyn Entity,
139        is_precise: bool,
140    ) {
141        if !is_precise {
142            return;
143        }
144
145        let drag_down = state.get_value(&BlockStateProperties::DRAG);
146        if Self::is_open_above(level, pos) {
147            entity.on_above_bubble_column(drag_down, pos);
148        } else {
149            entity.on_inside_bubble_column(drag_down);
150        }
151    }
152}
153
154impl BlockBehavior for BubbleColumnBlock {
155    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
156        Some(self.block.default_state())
157    }
158
159    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
160        let below = world.get_block_state(pos.below());
161        below.get_block() == self.block
162            || below
163                .get_block()
164                .has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_PUSH_UP)
165            || below
166                .get_block()
167                .has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_DRAG_DOWN)
168    }
169
170    fn update_shape(
171        &self,
172        state: BlockStateId,
173        world: &dyn ScheduledTickAccess,
174        pos: BlockPos,
175        direction: Direction,
176        _neighbor_pos: BlockPos,
177        neighbor_state: BlockStateId,
178    ) -> BlockStateId {
179        let delay = world.fluid_tick_delay(&vanilla_fluids::WATER);
180        let _ = world.schedule_fluid_tick_default(pos, &vanilla_fluids::WATER, delay);
181
182        if !self.can_survive(state, world, pos)
183            || direction == Direction::Down
184            || (direction == Direction::Up
185                && neighbor_state.get_block() != self.block
186                && Self::can_occupy(self.block, neighbor_state))
187        {
188            let _ = world.schedule_block_tick_default(pos, self.block, 5);
189        }
190
191        state
192    }
193
194    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
195        Self::update_column_with_state(
196            self.block,
197            world,
198            pos,
199            state,
200            world.get_block_state(pos.below()),
201        );
202    }
203
204    fn entity_inside(
205        &self,
206        state: BlockStateId,
207        world: &Arc<World>,
208        pos: BlockPos,
209        entity: &dyn Entity,
210        _effect_collector: &mut InsideBlockEffectCollector,
211        is_precise: bool,
212    ) {
213        Self::apply_entity_effect(state, world.as_ref(), pos, entity, is_precise);
214    }
215
216    fn pickup_block(
217        &self,
218        world: &Arc<World>,
219        pos: BlockPos,
220        state: BlockStateId,
221        _player: Option<&Player>,
222    ) -> Option<PickupResult> {
223        if world.set_block_if_unchanged(
224            pos,
225            state,
226            vanilla_blocks::AIR.default_state(),
227            UpdateFlags::UPDATE_ALL_IMMEDIATE,
228        ) != ConditionalBlockSetResult::Changed
229        {
230            return None;
231        }
232        Some(PickupResult {
233            filled_bucket: ItemStack::new(&vanilla_items::WATER_BUCKET),
234            sound: Some(&sound_events::ITEM_BUCKET_FILL),
235        })
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use std::sync::Weak;
242
243    use glam::DVec3;
244    use steel_registry::entity_type::EntityTypeRef;
245    use steel_registry::init_vanilla_registry;
246    use steel_registry::vanilla_entities;
247    use steel_utils::locks::SyncMutex;
248
249    use super::*;
250    use crate::behavior::init_behaviors;
251    use crate::entity::EntityBase;
252    use crate::test_support::TestLevel;
253
254    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
255    enum BubbleColumnCall {
256        Above { drag_down: bool, pos: BlockPos },
257        Inside { drag_down: bool },
258    }
259
260    struct RecordingEntity {
261        base: EntityBase,
262        calls: SyncMutex<Vec<BubbleColumnCall>>,
263    }
264
265    impl RecordingEntity {
266        fn new() -> Self {
267            Self {
268                base: EntityBase::new(
269                    1,
270                    DVec3::ZERO,
271                    vanilla_entities::ITEM.dimensions,
272                    Weak::new(),
273                ),
274                calls: SyncMutex::new(Vec::new()),
275            }
276        }
277
278        fn calls(&self) -> Vec<BubbleColumnCall> {
279            self.calls.lock().clone()
280        }
281    }
282
283    crate::entity::impl_test_downcast_type!(RecordingEntity);
284
285    impl Entity for RecordingEntity {
286        fn base(&self) -> &EntityBase {
287            &self.base
288        }
289
290        fn entity_type(&self) -> EntityTypeRef {
291            &vanilla_entities::ITEM
292        }
293
294        fn on_above_bubble_column(&self, drag_down: bool, pos: BlockPos) {
295            self.calls
296                .lock()
297                .push(BubbleColumnCall::Above { drag_down, pos });
298        }
299
300        fn on_inside_bubble_column(&self, drag_down: bool) {
301            self.calls
302                .lock()
303                .push(BubbleColumnCall::Inside { drag_down });
304        }
305    }
306
307    fn bubble_column_state(drag_down: bool) -> BlockStateId {
308        vanilla_blocks::BUBBLE_COLUMN
309            .default_state()
310            .set_value(&BlockStateProperties::DRAG, drag_down)
311    }
312
313    #[test]
314    fn bubble_column_update_shape_schedules_water_and_column_tick() {
315        init_vanilla_registry();
316        let behavior = BubbleColumnBlock::new(&vanilla_blocks::BUBBLE_COLUMN);
317        let level = TestLevel::default();
318        let state = vanilla_blocks::BUBBLE_COLUMN.default_state();
319
320        let updated = behavior.update_shape(
321            state,
322            &level,
323            BlockPos::ZERO,
324            Direction::Down,
325            BlockPos::ZERO.below(),
326            vanilla_blocks::SOUL_SAND.default_state(),
327        );
328
329        assert_eq!(updated, state);
330        assert!(level.scheduled_water_tick());
331        assert!(
332            level
333                .scheduled_block_ticks
334                .borrow()
335                .iter()
336                .any(|tick| tick.block == &vanilla_blocks::BUBBLE_COLUMN && tick.delay == 5)
337        );
338    }
339
340    #[test]
341    fn bubble_column_update_column_uses_push_up_and_drag_down_blocks() {
342        init_vanilla_registry();
343        init_behaviors();
344        let level = TestLevel::default()
345            .with_block(BlockPos::ZERO, vanilla_blocks::WATER.default_state())
346            .with_block(
347                BlockPos::ZERO.above(),
348                vanilla_blocks::WATER.default_state(),
349            );
350
351        BubbleColumnBlock::update_column(
352            &vanilla_blocks::BUBBLE_COLUMN,
353            &level,
354            BlockPos::ZERO,
355            vanilla_blocks::SOUL_SAND.default_state(),
356        );
357
358        let placed = level.placed_blocks.borrow();
359        assert_eq!(placed.len(), 2);
360        assert!(placed.iter().all(|placed| {
361            placed.state.get_block() == &vanilla_blocks::BUBBLE_COLUMN
362                && !placed.state.get_value(&BlockStateProperties::DRAG)
363        }));
364    }
365
366    #[test]
367    fn precise_entity_with_open_block_above_uses_above_bubble_column_hook() {
368        init_vanilla_registry();
369        init_behaviors();
370        let level = TestLevel::default();
371        let entity = RecordingEntity::new();
372        let pos = BlockPos::ZERO;
373
374        BubbleColumnBlock::apply_entity_effect(
375            bubble_column_state(false),
376            &level,
377            pos,
378            &entity,
379            true,
380        );
381
382        assert_eq!(
383            entity.calls(),
384            vec![BubbleColumnCall::Above {
385                drag_down: false,
386                pos
387            }]
388        );
389    }
390
391    #[test]
392    fn precise_entity_with_fluid_above_stays_inside_bubble_column() {
393        init_vanilla_registry();
394        init_behaviors();
395        let level = TestLevel::default().with_block(
396            BlockPos::ZERO.above(),
397            vanilla_blocks::WATER.default_state(),
398        );
399        let entity = RecordingEntity::new();
400
401        BubbleColumnBlock::apply_entity_effect(
402            bubble_column_state(true),
403            &level,
404            BlockPos::ZERO,
405            &entity,
406            true,
407        );
408
409        assert_eq!(
410            entity.calls(),
411            vec![BubbleColumnCall::Inside { drag_down: true }]
412        );
413    }
414
415    #[test]
416    fn imprecise_entity_does_not_apply_bubble_column_effect() {
417        init_vanilla_registry();
418        init_behaviors();
419        let level = TestLevel::default();
420        let entity = RecordingEntity::new();
421
422        BubbleColumnBlock::apply_entity_effect(
423            bubble_column_state(false),
424            &level,
425            BlockPos::ZERO,
426            &entity,
427            false,
428        );
429
430        assert_eq!(entity.calls(), Vec::new());
431    }
432}