1use std::sync::Arc;
4
5use glam::DVec3;
6use steel_registry::fluid::{FluidState, FluidStateExt as _};
7use steel_utils::{BlockPos, ChunkPos, SectionPos, WorldAabb, axis::Axis};
8
9use crate::fluid::{get_flow, get_fluid_state, get_height};
10use crate::world::World;
11
12const FLUID_INTERACTION_MARGIN: f64 = 0.001;
13const MIN_CURRENT_LENGTH_SQUARED: f64 = 1.0e-5;
14const STILL_CURRENT_VELOCITY_THRESHOLD: f64 = 0.003;
15const MIN_STILL_CURRENT_IMPULSE: f64 = 0.004_500_000_000_000_000_5;
16const SHALLOW_CURRENT_HEIGHT: f64 = 0.4;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq)]
19struct EntityFluidCurrent {
20 accumulated: DVec3,
21 count: u32,
22}
23
24impl EntityFluidCurrent {
25 fn accumulate(&mut self, flow: DVec3) {
26 self.accumulated += flow;
27 self.count += 1;
28 }
29
30 fn impulse(self, is_player: bool, old_velocity: DVec3, scale: f64) -> DVec3 {
31 if self.count == 0 || self.accumulated.length_squared() < MIN_CURRENT_LENGTH_SQUARED {
32 return DVec3::ZERO;
33 }
34
35 let mut impulse = if is_player {
36 self.accumulated / f64::from(self.count)
37 } else {
38 self.accumulated.normalize_or_zero()
39 };
40 impulse *= scale;
41
42 if old_velocity.x.abs() < STILL_CURRENT_VELOCITY_THRESHOLD
43 && old_velocity.z.abs() < STILL_CURRENT_VELOCITY_THRESHOLD
44 && impulse.length() < MIN_STILL_CURRENT_IMPULSE
45 {
46 impulse = impulse.normalize_or_zero() * MIN_STILL_CURRENT_IMPULSE;
47 }
48
49 impulse
50 }
51}
52
53#[derive(Debug, Clone, Copy)]
54struct FluidScanBounds {
55 interaction_box: WorldAabb,
56 entity_y: f64,
57 x0: i32,
58 y0: i32,
59 z0: i32,
60 x1: i32,
61 y1: i32,
62 z1: i32,
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq)]
71pub struct EntityFluidContact {
72 water_height: f64,
73 lava_height: f64,
74 eye_in_water: bool,
75 eye_in_lava: bool,
76 water_current: EntityFluidCurrent,
77 lava_current: EntityFluidCurrent,
78}
79
80impl EntityFluidContact {
81 #[cfg(test)]
82 #[must_use]
83 pub(crate) fn from_parts(
84 water_height: f64,
85 lava_height: f64,
86 eye_in_water: bool,
87 eye_in_lava: bool,
88 ) -> Self {
89 Self {
90 water_height,
91 lava_height,
92 eye_in_water,
93 eye_in_lava,
94 water_current: EntityFluidCurrent::default(),
95 lava_current: EntityFluidCurrent::default(),
96 }
97 }
98
99 #[must_use]
101 pub fn scan(world: &Arc<World>, position: DVec3, eye_y: f64, bounding_box: WorldAabb) -> Self {
102 let Some(bounds) = Self::scan_bounds(bounding_box) else {
103 return Self::default();
104 };
105 if !has_fluid_and_loaded(world, bounds) {
106 return Self::default();
107 }
108
109 Self::scan_with_bounds(
110 bounds,
111 position,
112 eye_y,
113 false,
114 |pos| get_fluid_state(world, pos),
115 |pos, fluid_state| get_height(world, pos, fluid_state),
116 |_pos, _fluid_state| DVec3::ZERO,
117 )
118 }
119
120 #[must_use]
122 pub fn scan_with_currents(
123 world: &Arc<World>,
124 position: DVec3,
125 eye_y: f64,
126 bounding_box: WorldAabb,
127 include_current: bool,
128 ) -> Self {
129 let Some(bounds) = Self::scan_bounds(bounding_box) else {
130 return Self::default();
131 };
132 if !has_fluid_and_loaded(world, bounds) {
133 return Self::default();
134 }
135
136 Self::scan_with_bounds(
137 bounds,
138 position,
139 eye_y,
140 include_current,
141 |pos| get_fluid_state(world, pos),
142 |pos, fluid_state| get_height(world, pos, fluid_state),
143 |pos, fluid_state| get_flow(world, pos, fluid_state),
144 )
145 }
146
147 #[must_use]
149 pub const fn water_height(self) -> f64 {
150 self.water_height
151 }
152
153 #[must_use]
155 pub const fn lava_height(self) -> f64 {
156 self.lava_height
157 }
158
159 #[must_use]
161 pub const fn eye_in_water(self) -> bool {
162 self.eye_in_water
163 }
164
165 #[must_use]
167 pub const fn eye_in_lava(self) -> bool {
168 self.eye_in_lava
169 }
170
171 #[must_use]
173 pub fn water_current_impulse(self, is_player: bool, old_velocity: DVec3, scale: f64) -> DVec3 {
174 self.water_current.impulse(is_player, old_velocity, scale)
175 }
176
177 #[must_use]
179 pub fn lava_current_impulse(self, is_player: bool, old_velocity: DVec3, scale: f64) -> DVec3 {
180 self.lava_current.impulse(is_player, old_velocity, scale)
181 }
182
183 #[cfg(test)]
184 fn scan_with(
185 bounding_box: WorldAabb,
186 position: DVec3,
187 eye_y: f64,
188 fluid_at: impl FnMut(BlockPos) -> FluidState,
189 height_at: impl FnMut(BlockPos, FluidState) -> f32,
190 ) -> Self {
191 Self::scan_with_flow(
192 bounding_box,
193 position,
194 eye_y,
195 false,
196 fluid_at,
197 height_at,
198 |_pos, _fluid_state| DVec3::ZERO,
199 )
200 }
201
202 #[cfg(test)]
203 fn scan_with_flow(
204 bounding_box: WorldAabb,
205 position: DVec3,
206 eye_y: f64,
207 include_current: bool,
208 fluid_at: impl FnMut(BlockPos) -> FluidState,
209 height_at: impl FnMut(BlockPos, FluidState) -> f32,
210 flow_at: impl FnMut(BlockPos, FluidState) -> DVec3,
211 ) -> Self {
212 let Some(bounds) = Self::scan_bounds(bounding_box) else {
213 return Self::default();
214 };
215
216 Self::scan_with_bounds(
217 bounds,
218 position,
219 eye_y,
220 include_current,
221 fluid_at,
222 height_at,
223 flow_at,
224 )
225 }
226
227 fn scan_bounds(bounding_box: WorldAabb) -> Option<FluidScanBounds> {
228 let interaction_box = bounding_box.deflate(FLUID_INTERACTION_MARGIN);
229 if interaction_box.is_empty() {
230 return None;
231 }
232
233 let x0 = interaction_box.min(Axis::X).floor() as i32;
234 let y0 = interaction_box.min(Axis::Y).floor() as i32;
235 let z0 = interaction_box.min(Axis::Z).floor() as i32;
236 let x1 = interaction_box.max(Axis::X).ceil() as i32 - 1;
237 let y1 = interaction_box.max(Axis::Y).ceil() as i32 - 1;
238 let z1 = interaction_box.max(Axis::Z).ceil() as i32 - 1;
239 if x0 > x1 || y0 > y1 || z0 > z1 {
240 return None;
241 }
242
243 Some(FluidScanBounds {
244 interaction_box,
245 entity_y: bounding_box.min(Axis::Y),
246 x0,
247 y0,
248 z0,
249 x1,
250 y1,
251 z1,
252 })
253 }
254
255 fn scan_with_bounds(
256 bounds: FluidScanBounds,
257 position: DVec3,
258 eye_y: f64,
259 include_current: bool,
260 mut fluid_at: impl FnMut(BlockPos) -> FluidState,
261 mut height_at: impl FnMut(BlockPos, FluidState) -> f32,
262 mut flow_at: impl FnMut(BlockPos, FluidState) -> DVec3,
263 ) -> Self {
264 let mut contact = Self::default();
265 let eye_block_x = position.x.floor() as i32;
266 let eye_block_z = position.z.floor() as i32;
267
268 for x in bounds.x0..=bounds.x1 {
269 for y in bounds.y0..=bounds.y1 {
270 for z in bounds.z0..=bounds.z1 {
271 let pos = BlockPos::new(x, y, z);
272 let fluid_state = fluid_at(pos);
273 if fluid_state.is_empty() {
274 continue;
275 }
276
277 let fluid_bottom = f64::from(y);
278 let fluid_top = fluid_bottom + f64::from(height_at(pos, fluid_state));
279 if fluid_top < bounds.interaction_box.min(Axis::Y) {
280 continue;
281 }
282
283 let eye_inside = x == eye_block_x
284 && z == eye_block_z
285 && eye_y >= fluid_bottom
286 && eye_y <= fluid_top;
287 let height = fluid_top - bounds.entity_y;
288 if fluid_state.is_water() {
289 contact.water_height = contact.water_height.max(height);
290 contact.eye_in_water |= eye_inside;
291 if include_current {
292 let mut flow = flow_at(pos, fluid_state);
293 if contact.water_height < SHALLOW_CURRENT_HEIGHT {
294 flow *= contact.water_height;
295 }
296 contact.water_current.accumulate(flow);
297 }
298 } else if fluid_state.is_lava() {
299 contact.lava_height = contact.lava_height.max(height);
300 contact.eye_in_lava |= eye_inside;
301 if include_current {
302 let mut flow = flow_at(pos, fluid_state);
303 if contact.lava_height < SHALLOW_CURRENT_HEIGHT {
304 flow *= contact.lava_height;
305 }
306 contact.lava_current.accumulate(flow);
307 }
308 }
309 }
310 }
311 }
312
313 contact
314 }
315}
316
317#[expect(
318 clippy::similar_names,
319 reason = "axis-paired bounds mirror vanilla hasFluidAndLoaded"
320)]
321fn has_fluid_and_loaded(world: &World, bounds: FluidScanBounds) -> bool {
322 let section_x0 = SectionPos::block_to_section_coord(bounds.x0 - 1);
323 let section_y0 = SectionPos::block_to_section_coord(bounds.y0);
324 let section_z0 = SectionPos::block_to_section_coord(bounds.z0 - 1);
325 let section_x1 = SectionPos::block_to_section_coord(bounds.x1 + 1);
326 let section_y1 = SectionPos::block_to_section_coord(bounds.y1);
327 let section_z1 = SectionPos::block_to_section_coord(bounds.z1 + 1);
328
329 let mut has_fluid = false;
330 for chunk_z in section_z0..=section_z1 {
331 for chunk_x in section_x0..=section_x1 {
332 let Some(chunk_has_fluid) =
333 world
334 .chunk_map
335 .with_full_chunk(ChunkPos::new(chunk_x, chunk_z), |chunk| {
336 let min_section_y = SectionPos::block_to_section_coord(chunk.min_y());
337 let sections = &chunk.sections().sections;
338 let mut chunk_has_fluid = false;
339
340 for section_y in section_y0..=section_y1 {
341 let section_index = section_y - min_section_y;
342 let Ok(section_index) = usize::try_from(section_index) else {
343 continue;
344 };
345 let Some(section) = sections.get(section_index) else {
346 continue;
347 };
348
349 chunk_has_fluid |= section.read().has_fluid();
350 }
351
352 chunk_has_fluid
353 })
354 else {
355 return false;
356 };
357
358 has_fluid |= chunk_has_fluid;
359 }
360 }
361
362 has_fluid
363}
364
365#[cfg(test)]
366mod tests {
367 use steel_registry::fluid::FluidState;
368 use steel_registry::init_vanilla_registry;
369 use steel_registry::vanilla_fluids;
370
371 use super::*;
372
373 #[test]
374 fn scan_reports_fluid_height_above_entity_feet() {
375 init_vanilla_registry();
376 let bounding_box = WorldAabb::new(0.1, 10.0, 0.1, 0.9, 10.5, 0.9);
377
378 let contact = EntityFluidContact::scan_with(
379 bounding_box,
380 DVec3::new(0.5, 10.0, 0.5),
381 12.0,
382 |pos| {
383 if pos.y() == 10 {
384 FluidState::source(&vanilla_fluids::WATER)
385 } else {
386 FluidState::EMPTY
387 }
388 },
389 |_pos, _fluid_state| 1.0,
390 );
391
392 assert!((contact.water_height() - 1.0).abs() < f64::EPSILON);
393 assert!(contact.lava_height().abs() < f64::EPSILON);
394 assert!(!contact.eye_in_water());
395 assert!(!contact.eye_in_lava());
396 }
397
398 #[test]
399 fn scan_uses_effective_fluid_height() {
400 init_vanilla_registry();
401 let bounding_box = WorldAabb::new(0.1, 10.0, 0.1, 0.9, 10.5, 0.9);
402
403 let contact = EntityFluidContact::scan_with(
404 bounding_box,
405 DVec3::new(0.5, 10.0, 0.5),
406 12.0,
407 |pos| {
408 if pos.y() == 10 {
409 FluidState::flowing(&vanilla_fluids::FLOWING_LAVA, 4, false)
410 } else {
411 FluidState::EMPTY
412 }
413 },
414 |_pos, _fluid_state| 4.0 / 9.0,
415 );
416
417 assert!(contact.water_height().abs() < f64::EPSILON);
418 assert!((contact.lava_height() - 4.0 / 9.0).abs() < 1.0e-7);
419 assert!(!contact.eye_in_water());
420 assert!(!contact.eye_in_lava());
421 }
422
423 #[test]
424 fn scan_ignores_fluid_below_interaction_box() {
425 init_vanilla_registry();
426 let bounding_box = WorldAabb::new(0.1, 10.2, 0.1, 0.9, 10.6, 0.9);
427
428 let contact = EntityFluidContact::scan_with(
429 bounding_box,
430 DVec3::new(0.5, 10.0, 0.5),
431 10.3,
432 |pos| {
433 if pos.y() == 10 {
434 FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 1, false)
435 } else {
436 FluidState::EMPTY
437 }
438 },
439 |_pos, _fluid_state| 1.0 / 9.0,
440 );
441
442 assert_eq!(contact, EntityFluidContact::default());
443 }
444
445 #[test]
446 fn scan_marks_eye_inside_matching_fluid_column() {
447 init_vanilla_registry();
448 let bounding_box = WorldAabb::new(0.1, 10.0, 0.1, 0.9, 11.0, 0.9);
449
450 let contact = EntityFluidContact::scan_with(
451 bounding_box,
452 DVec3::new(0.5, 10.0, 0.5),
453 10.8,
454 |pos| {
455 if pos.y() == 10 {
456 FluidState::source(&vanilla_fluids::WATER)
457 } else {
458 FluidState::EMPTY
459 }
460 },
461 |_pos, _fluid_state| 1.0,
462 );
463
464 assert!(contact.eye_in_water());
465 assert!(!contact.eye_in_lava());
466 }
467
468 #[test]
469 fn scan_accumulates_player_fluid_current_as_average_flow() {
470 init_vanilla_registry();
471 let bounding_box = WorldAabb::new(0.1, 10.0, 0.1, 1.9, 10.5, 0.9);
472
473 let contact = EntityFluidContact::scan_with_flow(
474 bounding_box,
475 DVec3::new(0.5, 10.0, 0.5),
476 12.0,
477 true,
478 |_pos| FluidState::source(&vanilla_fluids::WATER),
479 |_pos, _fluid_state| 1.0,
480 |pos, _fluid_state| {
481 if pos.x() == 0 { DVec3::X } else { DVec3::Z }
482 },
483 );
484
485 assert_eq!(
486 contact.water_current_impulse(true, DVec3::ZERO, 1.0),
487 DVec3::new(0.5, 0.0, 0.5)
488 );
489 }
490
491 #[test]
492 fn scan_accumulates_non_player_fluid_current_as_normalized_flow() {
493 init_vanilla_registry();
494 let bounding_box = WorldAabb::new(0.1, 10.0, 0.1, 1.9, 10.5, 0.9);
495
496 let contact = EntityFluidContact::scan_with_flow(
497 bounding_box,
498 DVec3::new(0.5, 10.0, 0.5),
499 12.0,
500 true,
501 |_pos| FluidState::source(&vanilla_fluids::WATER),
502 |_pos, _fluid_state| 1.0,
503 |pos, _fluid_state| {
504 if pos.x() == 0 { DVec3::X } else { DVec3::Z }
505 },
506 );
507
508 let expected = DVec3::new(1.0, 0.0, 1.0).normalize();
509 let impulse = contact.water_current_impulse(false, DVec3::ZERO, 1.0);
510 assert!((impulse - expected).length() < f64::EPSILON);
511 }
512
513 #[test]
514 fn shallow_current_is_scaled_by_fluid_height() {
515 init_vanilla_registry();
516 let bounding_box = WorldAabb::new(0.1, 10.0, 0.1, 0.9, 10.5, 0.9);
517
518 let contact = EntityFluidContact::scan_with_flow(
519 bounding_box,
520 DVec3::new(0.5, 10.0, 0.5),
521 12.0,
522 true,
523 |_pos| FluidState::source(&vanilla_fluids::WATER),
524 |_pos, _fluid_state| 0.2,
525 |_pos, _fluid_state| DVec3::X,
526 );
527
528 let impulse = contact.water_current_impulse(true, DVec3::new(0.01, 0.0, 0.0), 1.0);
529 assert!((impulse - DVec3::new(0.2, 0.0, 0.0)).length() < 1.0e-7);
530 }
531}