Skip to main content

steel_core/chunk/
heightmap.rs

1//! Heightmap implementation for tracking the highest blocks in a chunk.
2//!
3//! Heightmaps are used for various purposes like spawning, pathfinding, and rendering.
4//!
5//! `ChunkHeightmaps` stores the six vanilla heightmap slots across every chunk phase.
6//! Individual maps are materialized as required by the chunk's generation status.
7
8use std::sync::LazyLock;
9
10use smallvec::SmallVec;
11use steel_registry::{
12    REGISTRY,
13    blocks::{BlockRef, block_state_ext::BlockStateExt},
14    vanilla_block_tags::BlockTag,
15};
16use steel_utils::BlockStateId;
17
18/// The different types of heightmaps.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum HeightmapType {
21    // Final heightmaps (sent to client, used after CARVERS status)
22    /// Tracks the highest non-air block. Used for world surface calculations.
23    WorldSurface,
24    /// Tracks the highest motion-blocking block (solid or fluid).
25    MotionBlocking,
26    /// Tracks the highest motion-blocking block that is not leaves.
27    MotionBlockingNoLeaves,
28    /// Tracks the highest solid block (ocean floor).
29    OceanFloor,
30    // Worldgen heightmaps (used before CARVERS status)
31    /// Worldgen version of `WorldSurface`.
32    WorldSurfaceWg,
33    /// Worldgen version of `OceanFloor`.
34    OceanFloorWg,
35}
36
37impl HeightmapType {
38    const WORLD_SURFACE_MASK: u8 = 1 << 0;
39    const MOTION_BLOCKING_MASK: u8 = 1 << 1;
40    const MOTION_BLOCKING_NO_LEAVES_MASK: u8 = 1 << 2;
41    const OCEAN_FLOOR_MASK: u8 = 1 << 3;
42    const WORLD_SURFACE_WG_MASK: u8 = 1 << 4;
43    const OCEAN_FLOOR_WG_MASK: u8 = 1 << 5;
44
45    /// Returns worldgen heightmap types (used before CARVERS status).
46    #[must_use]
47    pub const fn worldgen_types() -> &'static [HeightmapType] {
48        &[HeightmapType::WorldSurfaceWg, HeightmapType::OceanFloorWg]
49    }
50
51    /// Returns final heightmap types (used at CARVERS status and after).
52    #[must_use]
53    pub const fn final_types() -> &'static [HeightmapType] {
54        &[
55            HeightmapType::WorldSurface,
56            HeightmapType::MotionBlocking,
57            HeightmapType::MotionBlockingNoLeaves,
58            HeightmapType::OceanFloor,
59        ]
60    }
61
62    /// Returns all heightmap types in their stable persistence order.
63    #[must_use]
64    pub const fn all_types() -> &'static [HeightmapType] {
65        &[
66            HeightmapType::WorldSurface,
67            HeightmapType::MotionBlocking,
68            HeightmapType::MotionBlockingNoLeaves,
69            HeightmapType::OceanFloor,
70            HeightmapType::WorldSurfaceWg,
71            HeightmapType::OceanFloorWg,
72        ]
73    }
74
75    #[must_use]
76    pub(crate) const fn persistence_id(self) -> u8 {
77        match self {
78            Self::WorldSurface => 0,
79            Self::MotionBlocking => 1,
80            Self::MotionBlockingNoLeaves => 2,
81            Self::OceanFloor => 3,
82            Self::WorldSurfaceWg => 4,
83            Self::OceanFloorWg => 5,
84        }
85    }
86
87    #[must_use]
88    pub(crate) const fn from_persistence_id(id: u8) -> Option<Self> {
89        match id {
90            0 => Some(Self::WorldSurface),
91            1 => Some(Self::MotionBlocking),
92            2 => Some(Self::MotionBlockingNoLeaves),
93            3 => Some(Self::OceanFloor),
94            4 => Some(Self::WorldSurfaceWg),
95            5 => Some(Self::OceanFloorWg),
96            _ => None,
97        }
98    }
99
100    /// Returns whether a block is "opaque" for this heightmap type.
101    /// This determines whether the block counts towards the heightmap.
102    ///
103    /// # Panics
104    /// Panics if the block state ID is invalid.
105    #[must_use]
106    pub fn is_opaque(self, state: BlockStateId) -> bool {
107        heightmap_opacity_mask(state, self.mask()) != 0
108    }
109
110    /// Checks if a block is in the leaves tag.
111    fn is_leaves(block: BlockRef) -> bool {
112        block.has_tag(&BlockTag::LEAVES)
113    }
114
115    const fn mask(self) -> u8 {
116        match self {
117            Self::WorldSurface => Self::WORLD_SURFACE_MASK,
118            Self::MotionBlocking => Self::MOTION_BLOCKING_MASK,
119            Self::MotionBlockingNoLeaves => Self::MOTION_BLOCKING_NO_LEAVES_MASK,
120            Self::OceanFloor => Self::OCEAN_FLOOR_MASK,
121            Self::WorldSurfaceWg => Self::WORLD_SURFACE_WG_MASK,
122            Self::OceanFloorWg => Self::OCEAN_FLOOR_WG_MASK,
123        }
124    }
125}
126
127static WORLD_SURFACE_OPACITY_MASK_BY_STATE: LazyLock<Box<[u8]>> =
128    LazyLock::new(build_world_surface_opacity_masks);
129static HEIGHTMAP_OPACITY_MASK_BY_STATE: LazyLock<Box<[u8]>> =
130    LazyLock::new(build_state_opacity_masks);
131
132fn build_world_surface_opacity_masks() -> Box<[u8]> {
133    let mut masks = Vec::with_capacity(REGISTRY.blocks.state_to_block_lookup.len());
134    for (state_index, &block) in REGISTRY.blocks.state_to_block_lookup.iter().enumerate() {
135        let Ok(_) = u16::try_from(state_index) else {
136            panic!("block state registry exceeded BlockStateId range");
137        };
138        let mask = if block.config.is_air {
139            0
140        } else {
141            HeightmapType::WORLD_SURFACE_MASK | HeightmapType::WORLD_SURFACE_WG_MASK
142        };
143        masks.push(mask);
144    }
145    masks.into_boxed_slice()
146}
147
148fn build_state_opacity_masks() -> Box<[u8]> {
149    let mut masks = Vec::with_capacity(REGISTRY.blocks.state_to_block_lookup.len());
150    for (state_index, &block) in REGISTRY.blocks.state_to_block_lookup.iter().enumerate() {
151        let Ok(raw_state_id) = u16::try_from(state_index) else {
152            panic!("block state registry exceeded BlockStateId range");
153        };
154        let state = BlockStateId(raw_state_id);
155        let mut mask = 0;
156        if !block.config.is_air {
157            mask |= HeightmapType::WORLD_SURFACE_MASK | HeightmapType::WORLD_SURFACE_WG_MASK;
158
159            let blocks_motion = BlockStateExt::blocks_motion(&state);
160            if blocks_motion {
161                mask |= HeightmapType::OCEAN_FLOOR_MASK | HeightmapType::OCEAN_FLOOR_WG_MASK;
162            }
163
164            if blocks_motion || state.has_fluid() {
165                mask |= HeightmapType::MOTION_BLOCKING_MASK;
166                if !HeightmapType::is_leaves(block) {
167                    mask |= HeightmapType::MOTION_BLOCKING_NO_LEAVES_MASK;
168                }
169            }
170        }
171        masks.push(mask);
172    }
173    masks.into_boxed_slice()
174}
175
176#[inline]
177fn heightmap_opacity_mask(state: BlockStateId, requested_mask: u8) -> u8 {
178    let world_surface_mask =
179        HeightmapType::WORLD_SURFACE_MASK | HeightmapType::WORLD_SURFACE_WG_MASK;
180    if requested_mask & !world_surface_mask == 0 {
181        let Some(&state_mask) = WORLD_SURFACE_OPACITY_MASK_BY_STATE.get(state.0 as usize) else {
182            panic!("invalid block state id {}", state.0);
183        };
184        return state_mask & requested_mask;
185    }
186
187    let Some(&state_mask) = HEIGHTMAP_OPACITY_MASK_BY_STATE.get(state.0 as usize) else {
188        panic!("invalid block state id {}", state.0);
189    };
190    state_mask & requested_mask
191}
192
193/// A heightmap that tracks the highest blocks of a specific type in a chunk.
194///
195/// The heightmap stores heights for each column in a 16x16 chunk.
196/// Heights are stored relative to `min_y`, so `data[index] + min_y` gives the actual Y coordinate.
197#[derive(Debug, Clone)]
198pub struct Heightmap {
199    /// Height data stored as a flat array of 256 entries (16x16).
200    /// Each entry stores the height relative to `min_y`.
201    data: Box<[u16; 256]>,
202    /// The type of this heightmap.
203    map_type: HeightmapType,
204    /// The minimum Y coordinate of the world.
205    min_y: i32,
206    /// The total height of the world.
207    height: i32,
208}
209
210impl Heightmap {
211    /// Creates a new heightmap with all heights initialized to `min_y`.
212    #[must_use]
213    pub fn new(map_type: HeightmapType, min_y: i32, height: i32) -> Self {
214        Self {
215            data: Box::new([0; 256]),
216            map_type,
217            min_y,
218            height,
219        }
220    }
221
222    /// Creates a heightmap from raw height data loaded from disk.
223    #[must_use]
224    pub const fn from_raw_data(
225        map_type: HeightmapType,
226        min_y: i32,
227        height: i32,
228        data: Box<[u16; 256]>,
229    ) -> Self {
230        Self {
231            data,
232            map_type,
233            min_y,
234            height,
235        }
236    }
237
238    /// Returns the heightmap type.
239    #[must_use]
240    pub const fn heightmap_type(&self) -> HeightmapType {
241        self.map_type
242    }
243
244    /// Gets the index into the data array for the given local coordinates.
245    #[inline]
246    const fn get_index(local_x: usize, local_z: usize) -> usize {
247        local_x + local_z * 16
248    }
249
250    /// Gets the first available Y coordinate (one above the highest block) at the given position.
251    #[must_use]
252    pub fn get_first_available(&self, local_x: usize, local_z: usize) -> i32 {
253        debug_assert!(local_x < 16 && local_z < 16);
254        let index = Self::get_index(local_x, local_z);
255        i32::from(self.data[index]) + self.min_y
256    }
257
258    /// Gets the highest taken Y coordinate at the given position.
259    #[must_use]
260    pub fn get_highest_taken(&self, local_x: usize, local_z: usize) -> i32 {
261        self.get_first_available(local_x, local_z) - 1
262    }
263
264    /// Sets the height at the given position.
265    pub fn set_height(&mut self, local_x: usize, local_z: usize, height: i32) {
266        debug_assert!(local_x < 16 && local_z < 16);
267        let index = Self::get_index(local_x, local_z);
268        self.data[index] = (height - self.min_y) as u16;
269    }
270
271    /// Updates the heightmap when a block changes.
272    ///
273    /// Returns `true` if the heightmap was modified.
274    ///
275    /// # Arguments
276    /// * `local_x` - The local X coordinate (0-15)
277    /// * `y` - The absolute Y coordinate
278    /// * `local_z` - The local Z coordinate (0-15)
279    /// * `state` - The new block state at this position
280    /// * `get_block` - A function to get block states at other positions for scanning down
281    pub fn update<F>(
282        &mut self,
283        local_x: usize,
284        y: i32,
285        local_z: usize,
286        state: BlockStateId,
287        get_block: F,
288    ) -> bool
289    where
290        F: Fn(usize, i32, usize) -> BlockStateId,
291    {
292        let first_available = self.get_first_available(local_x, local_z);
293
294        // If the block is well below the current height, it can't affect the heightmap
295        if y <= first_available - 2 {
296            return false;
297        }
298
299        if self.map_type.is_opaque(state) {
300            // Block is opaque - if it's at or above current height, update
301            if y >= first_available {
302                self.set_height(local_x, local_z, y + 1);
303                return true;
304            }
305        } else if first_available - 1 == y {
306            // Block is not opaque and is at the current top - scan down to find new height
307            for scan_y in (self.min_y..y).rev() {
308                let scan_state = get_block(local_x, scan_y, local_z);
309                if self.map_type.is_opaque(scan_state) {
310                    self.set_height(local_x, local_z, scan_y + 1);
311                    return true;
312                }
313            }
314            // No opaque block found, set to min_y
315            self.set_height(local_x, local_z, self.min_y);
316            return true;
317        }
318
319        false
320    }
321
322    /// Updates this heightmap for a direct write into a previously-air block.
323    ///
324    /// Vanilla's noise fill writes sections directly and updates the worldgen
325    /// heightmaps beside those writes. There is no downward scan in that path
326    /// because blocks are only being added to an empty terrain column.
327    pub fn update_for_initial_fill(
328        &mut self,
329        local_x: usize,
330        y: i32,
331        local_z: usize,
332        state: BlockStateId,
333    ) -> bool {
334        let first_available = self.get_first_available(local_x, local_z);
335        if self.map_type.is_opaque(state) && y >= first_available {
336            self.set_height(local_x, local_z, y + 1);
337            return true;
338        }
339
340        false
341    }
342
343    /// Returns a direct reference to the raw height data array.
344    ///
345    /// Values are stored relative to `min_y`. Used for persistence.
346    #[must_use]
347    pub fn raw_data(&self) -> &[u16; 256] {
348        &self.data
349    }
350
351    /// Gets the raw data as a slice of i64 values for network serialization.
352    ///
353    /// The data is packed using the minimum number of bits required to store
354    /// the height range (0 to `world_height`).
355    #[must_use]
356    pub fn get_raw_data(&self) -> Vec<i64> {
357        let bits_per_value = Self::calculate_bits_per_value(self.height);
358        let values_per_long = 64 / bits_per_value;
359        let num_longs = 256_usize.div_ceil(values_per_long);
360
361        let mut result = vec![0i64; num_longs];
362        let mask = (1u64 << bits_per_value) - 1;
363
364        for (i, &height) in self.data.iter().enumerate() {
365            let long_index = i / values_per_long;
366            let bit_offset = (i % values_per_long) * bits_per_value;
367            result[long_index] |= ((u64::from(height) & mask) << bit_offset) as i64;
368        }
369
370        result
371    }
372
373    /// Calculates the number of bits required to store heights for a given world height.
374    #[inline]
375    const fn calculate_bits_per_value(height: i32) -> usize {
376        // Need to store values from 0 to height (inclusive)
377        // ceil(log2(height + 1))
378        let max_value = height as u32 + 1;
379        if max_value <= 1 {
380            1
381        } else {
382            32 - (max_value - 1).leading_zeros() as usize
383        }
384    }
385}
386
387// ─── ChunkHeightmaps ─────────────────────────────────────────────────────────
388
389/// Heightmap storage retained across every phase of a chunk.
390///
391/// Stores heightmaps as `Option` fields since they are lazily initialized
392/// based on the chunk's generation status. Worldgen types (`WorldSurfaceWg`,
393/// `OceanFloorWg`) are used before CARVERS; final types are used after.
394#[derive(Debug, Clone)]
395pub struct ChunkHeightmaps {
396    world_surface_wg: Option<Heightmap>,
397    ocean_floor_wg: Option<Heightmap>,
398    world_surface: Option<Heightmap>,
399    motion_blocking: Option<Heightmap>,
400    motion_blocking_no_leaves: Option<Heightmap>,
401    ocean_floor: Option<Heightmap>,
402}
403
404impl ChunkHeightmaps {
405    /// Creates empty heightmap storage with no types initialized.
406    #[must_use]
407    pub const fn empty() -> Self {
408        Self {
409            world_surface_wg: None,
410            ocean_floor_wg: None,
411            world_surface: None,
412            motion_blocking: None,
413            motion_blocking_no_leaves: None,
414            ocean_floor: None,
415        }
416    }
417
418    /// Creates heightmap storage with every final chunk map initialized.
419    #[must_use]
420    pub fn new(min_y: i32, height: i32) -> Self {
421        Self::with_types(HeightmapType::final_types(), min_y, height)
422    }
423
424    /// Creates storage with the requested heightmap types initialized at `min_y`.
425    #[must_use]
426    pub fn with_types(types: &[HeightmapType], min_y: i32, height: i32) -> Self {
427        let mut heightmaps = Self::empty();
428        for &heightmap_type in types {
429            heightmaps.get_or_insert(heightmap_type, min_y, height);
430        }
431        heightmaps
432    }
433
434    /// Returns a reference to a heightmap by type, if it exists.
435    #[must_use]
436    pub const fn get(&self, heightmap_type: HeightmapType) -> Option<&Heightmap> {
437        match heightmap_type {
438            HeightmapType::WorldSurfaceWg => self.world_surface_wg.as_ref(),
439            HeightmapType::OceanFloorWg => self.ocean_floor_wg.as_ref(),
440            HeightmapType::WorldSurface => self.world_surface.as_ref(),
441            HeightmapType::MotionBlocking => self.motion_blocking.as_ref(),
442            HeightmapType::MotionBlockingNoLeaves => self.motion_blocking_no_leaves.as_ref(),
443            HeightmapType::OceanFloor => self.ocean_floor.as_ref(),
444        }
445    }
446
447    /// Returns a mutable reference to a heightmap by type, if it exists.
448    #[must_use]
449    pub const fn get_mut(&mut self, heightmap_type: HeightmapType) -> Option<&mut Heightmap> {
450        match heightmap_type {
451            HeightmapType::WorldSurfaceWg => self.world_surface_wg.as_mut(),
452            HeightmapType::OceanFloorWg => self.ocean_floor_wg.as_mut(),
453            HeightmapType::WorldSurface => self.world_surface.as_mut(),
454            HeightmapType::MotionBlocking => self.motion_blocking.as_mut(),
455            HeightmapType::MotionBlockingNoLeaves => self.motion_blocking_no_leaves.as_mut(),
456            HeightmapType::OceanFloor => self.ocean_floor.as_mut(),
457        }
458    }
459
460    /// Replaces one stored heightmap with a fully built instance.
461    pub fn replace(&mut self, heightmap: Heightmap) {
462        let heightmap_type = heightmap.heightmap_type();
463        match heightmap_type {
464            HeightmapType::WorldSurfaceWg => self.world_surface_wg = Some(heightmap),
465            HeightmapType::OceanFloorWg => self.ocean_floor_wg = Some(heightmap),
466            HeightmapType::WorldSurface => self.world_surface = Some(heightmap),
467            HeightmapType::MotionBlocking => self.motion_blocking = Some(heightmap),
468            HeightmapType::MotionBlockingNoLeaves => {
469                self.motion_blocking_no_leaves = Some(heightmap);
470            }
471            HeightmapType::OceanFloor => self.ocean_floor = Some(heightmap),
472        }
473    }
474
475    /// Returns a mutable reference to a heightmap, creating it if it doesn't exist.
476    fn get_or_insert(
477        &mut self,
478        heightmap_type: HeightmapType,
479        min_y: i32,
480        height: i32,
481    ) -> &mut Heightmap {
482        let slot = match heightmap_type {
483            HeightmapType::WorldSurfaceWg => &mut self.world_surface_wg,
484            HeightmapType::OceanFloorWg => &mut self.ocean_floor_wg,
485            HeightmapType::WorldSurface => &mut self.world_surface,
486            HeightmapType::MotionBlocking => &mut self.motion_blocking,
487            HeightmapType::MotionBlockingNoLeaves => &mut self.motion_blocking_no_leaves,
488            HeightmapType::OceanFloor => &mut self.ocean_floor,
489        };
490        slot.get_or_insert_with(|| Heightmap::new(heightmap_type, min_y, height))
491    }
492
493    /// Returns a final heightmap required by a full chunk.
494    ///
495    /// # Panics
496    /// Panics if a worldgen type is requested or the final map is missing.
497    #[must_use]
498    pub fn get_final(&self, heightmap_type: HeightmapType) -> &Heightmap {
499        if matches!(
500            heightmap_type,
501            HeightmapType::WorldSurfaceWg | HeightmapType::OceanFloorWg
502        ) {
503            panic!("worldgen heightmap {heightmap_type:?} is not a final chunk heightmap");
504        }
505        let Some(heightmap) = self.get(heightmap_type) else {
506            panic!("full chunk is missing required heightmap {heightmap_type:?}");
507        };
508        heightmap
509    }
510
511    /// Returns a mutable final heightmap required by a full chunk.
512    ///
513    /// # Panics
514    /// Panics if a worldgen type is requested or the final map is missing.
515    #[must_use]
516    pub fn get_final_mut(&mut self, heightmap_type: HeightmapType) -> &mut Heightmap {
517        if matches!(
518            heightmap_type,
519            HeightmapType::WorldSurfaceWg | HeightmapType::OceanFloorWg
520        ) {
521            panic!("worldgen heightmap {heightmap_type:?} is not a final chunk heightmap");
522        }
523        let Some(heightmap) = self.get_mut(heightmap_type) else {
524            panic!("full chunk is missing required heightmap {heightmap_type:?}");
525        };
526        heightmap
527    }
528
529    /// Updates every final heightmap after a full-chunk block change.
530    ///
531    /// # Panics
532    /// Panics if any required final heightmap is missing.
533    pub fn update_final<F>(
534        &mut self,
535        local_x: usize,
536        y: i32,
537        local_z: usize,
538        state: BlockStateId,
539        get_block: F,
540    ) where
541        F: Fn(usize, i32, usize) -> BlockStateId + Copy,
542    {
543        for &heightmap_type in HeightmapType::final_types() {
544            self.get_final_mut(heightmap_type)
545                .update(local_x, y, local_z, state, get_block);
546        }
547    }
548
549    fn set_primed_height(
550        &mut self,
551        heightmap_type: HeightmapType,
552        local_x: usize,
553        local_z: usize,
554        height: i32,
555    ) {
556        let Some(heightmap) = self.get_mut(heightmap_type) else {
557            panic!("heightmap {heightmap_type:?} missing after priming");
558        };
559        heightmap.set_height(local_x, local_z, height);
560    }
561
562    /// Primes missing heightmaps by scanning each section under one read lock.
563    pub fn prime_from_sections(
564        &mut self,
565        types: &[HeightmapType],
566        min_y: i32,
567        height: i32,
568        sections: &[super::section::SectionHolder],
569    ) {
570        let mut types_to_prime = SmallVec::<[(HeightmapType, u8); 4]>::new();
571        let mut pending_mask_base = 0;
572        for &hm_type in types {
573            if self.get(hm_type).is_none() {
574                let mask = hm_type.mask();
575                types_to_prime.push((hm_type, mask));
576                pending_mask_base |= mask;
577            }
578        }
579
580        if types_to_prime.is_empty() {
581            return;
582        }
583
584        for &(hm_type, _) in &types_to_prime {
585            self.get_or_insert(hm_type, min_y, height);
586        }
587
588        let mut pending_masks = [pending_mask_base; 16 * 16];
589        let mut pending_columns = pending_masks.len();
590
591        'sections: for section_idx in (0..sections.len()).rev() {
592            let guard = sections[section_idx].read();
593            if matches!(
594                &guard.states,
595                super::paletted_container::BlockPalette::Homogeneous(state) if state.is_air()
596            ) {
597                continue;
598            }
599
600            // Paletted block storage is contiguous in y-z-x order.
601            for local_y in (0..16).rev() {
602                let y = min_y + (section_idx * 16 + local_y) as i32;
603                let layer_start = local_y * 16 * 16;
604
605                for (column_index, pending_mask) in pending_masks.iter_mut().enumerate() {
606                    if *pending_mask == 0 {
607                        continue;
608                    }
609
610                    let state = guard.states.get_at_index(layer_start + column_index);
611                    let matched_mask = heightmap_opacity_mask(state, *pending_mask);
612                    if matched_mask == 0 {
613                        continue;
614                    }
615
616                    let x = column_index % 16;
617                    let z = column_index / 16;
618                    for &(hm_type, mask) in &types_to_prime {
619                        if matched_mask & mask != 0 {
620                            self.set_primed_height(hm_type, x, z, y + 1);
621                        }
622                    }
623                    *pending_mask &= !matched_mask;
624                    if *pending_mask == 0 {
625                        pending_columns -= 1;
626                    }
627                }
628
629                if pending_columns == 0 {
630                    break 'sections;
631                }
632            }
633        }
634    }
635}
636
637impl Default for ChunkHeightmaps {
638    fn default() -> Self {
639        Self::empty()
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use std::sync::Once;
646
647    use steel_registry::{
648        blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
649        init_vanilla_registry, vanilla_blocks,
650    };
651
652    use crate::behavior::init_behaviors;
653    use crate::chunk::section::{ChunkSection, Sections};
654
655    use super::*;
656
657    static INIT_BEHAVIORS: Once = Once::new();
658
659    fn init_test_state() {
660        init_vanilla_registry();
661        INIT_BEHAVIORS.call_once(init_behaviors);
662    }
663
664    #[test]
665    fn test_bits_per_value() {
666        // Standard overworld height (384 blocks: -64 to 319)
667        assert_eq!(Heightmap::calculate_bits_per_value(384), 9);
668        // Nether height (256 blocks)
669        assert_eq!(Heightmap::calculate_bits_per_value(256), 9);
670        // Small height
671        assert_eq!(Heightmap::calculate_bits_per_value(16), 5);
672    }
673
674    #[test]
675    fn test_get_index() {
676        assert_eq!(Heightmap::get_index(0, 0), 0);
677        assert_eq!(Heightmap::get_index(15, 0), 15);
678        assert_eq!(Heightmap::get_index(0, 1), 16);
679        assert_eq!(Heightmap::get_index(15, 15), 255);
680    }
681
682    #[test]
683    fn heightmap_predicates_use_blocks_motion_and_fluid_state() {
684        init_test_state();
685
686        let water = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
687        assert!(!HeightmapType::OceanFloorWg.is_opaque(water));
688        assert!(HeightmapType::MotionBlocking.is_opaque(water));
689
690        let slab = REGISTRY
691            .blocks
692            .get_default_state_id(&vanilla_blocks::OAK_SLAB);
693        let waterlogged_slab = slab.set_value(&BlockStateProperties::WATERLOGGED, true);
694        assert!(waterlogged_slab.has_fluid());
695        assert!(HeightmapType::MotionBlocking.is_opaque(waterlogged_slab));
696
697        let cobweb = REGISTRY
698            .blocks
699            .get_default_state_id(&vanilla_blocks::COBWEB);
700        assert!(!HeightmapType::OceanFloorWg.is_opaque(cobweb));
701    }
702
703    #[test]
704    fn initial_fill_update_tracks_only_matching_blocks() {
705        init_test_state();
706
707        let water = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
708        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
709
710        let mut ocean_floor = Heightmap::new(HeightmapType::OceanFloorWg, 0, 16);
711        assert!(!ocean_floor.update_for_initial_fill(0, 12, 0, water));
712        assert_eq!(ocean_floor.get_first_available(0, 0), 0);
713
714        assert!(ocean_floor.update_for_initial_fill(0, 5, 0, stone));
715        assert_eq!(ocean_floor.get_first_available(0, 0), 6);
716
717        assert!(!ocean_floor.update_for_initial_fill(0, 4, 0, stone));
718        assert_eq!(ocean_floor.get_first_available(0, 0), 6);
719    }
720
721    #[test]
722    fn section_priming_preserves_heightmap_predicates_across_sections() {
723        init_test_state();
724
725        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
726        let water = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
727        let leaves = REGISTRY
728            .blocks
729            .get_default_state_id(&vanilla_blocks::OAK_LEAVES);
730        let cobweb = REGISTRY
731            .blocks
732            .get_default_state_id(&vanilla_blocks::COBWEB);
733        let mut lower = ChunkSection::new_empty();
734        let mut upper = ChunkSection::new_empty();
735
736        lower.set_block_state(0, 15, 0, stone);
737        upper.set_block_state(0, 10, 0, water);
738        upper.set_block_state(1, 4, 2, stone);
739        upper.set_block_state(1, 12, 2, leaves);
740        upper.set_block_state(3, 3, 4, stone);
741        upper.set_block_state(3, 14, 4, cobweb);
742
743        let sections = Sections::from_owned(vec![lower, upper].into_boxed_slice());
744        let mut heightmaps = ChunkHeightmaps::empty();
745        heightmaps.prime_from_sections(
746            &[
747                HeightmapType::WorldSurface,
748                HeightmapType::MotionBlocking,
749                HeightmapType::MotionBlockingNoLeaves,
750                HeightmapType::OceanFloor,
751                HeightmapType::WorldSurfaceWg,
752                HeightmapType::OceanFloorWg,
753            ],
754            -16,
755            32,
756            &sections.sections,
757        );
758
759        let first_available = |heightmap_type, x, z| {
760            let Some(heightmap) = heightmaps.get(heightmap_type) else {
761                panic!("heightmap {heightmap_type:?} was not primed");
762            };
763            heightmap.get_first_available(x, z)
764        };
765
766        assert_eq!(first_available(HeightmapType::WorldSurface, 0, 0), 11);
767        assert_eq!(first_available(HeightmapType::MotionBlocking, 0, 0), 11);
768        assert_eq!(
769            first_available(HeightmapType::MotionBlockingNoLeaves, 0, 0),
770            11
771        );
772        assert_eq!(first_available(HeightmapType::OceanFloor, 0, 0), 0);
773        assert_eq!(first_available(HeightmapType::WorldSurfaceWg, 0, 0), 11);
774        assert_eq!(first_available(HeightmapType::OceanFloorWg, 0, 0), 0);
775
776        assert_eq!(first_available(HeightmapType::WorldSurface, 1, 2), 13);
777        assert_eq!(first_available(HeightmapType::MotionBlocking, 1, 2), 13);
778        assert_eq!(
779            first_available(HeightmapType::MotionBlockingNoLeaves, 1, 2),
780            5
781        );
782        assert_eq!(first_available(HeightmapType::OceanFloor, 1, 2), 13);
783
784        assert_eq!(first_available(HeightmapType::WorldSurface, 3, 4), 15);
785        assert_eq!(first_available(HeightmapType::MotionBlocking, 3, 4), 4);
786        assert_eq!(first_available(HeightmapType::OceanFloor, 3, 4), 4);
787        assert_eq!(first_available(HeightmapType::WorldSurface, 15, 15), -16);
788    }
789}