steel_core/behavior/blocks/fluid/
bubble_column_block.rs1use 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#[block_behavior]
29pub struct BubbleColumnBlock {
30 block: BlockRef,
31}
32
33const DRAG: &BoolProperty = &BlockStateProperties::DRAG;
34
35impl BubbleColumnBlock {
36 #[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}