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, 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
33impl BubbleColumnBlock {
34 #[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}