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