chunkgen 0.2.0

Minecraft world generation library
Documentation
use std::sync::OnceLock;

use parking_lot::RwLock;
use shulkr::util::Key;
use shulkr::world::block::{Block, BlockState};
use shulkr::world::chunk::{SECTION_SIZE, SECTION_VOLUME};
use shulkr::world::palette::Palette;

pub use shulkr::world::chunk::ChunkSection;

use shulkr::registry::registries;

use shulkr::util::BlockPosition;

pub fn state_from_id(state_id: u16) -> BlockState {
    BlockState::from_id(state_id).expect("stored state ids come from registered block states")
}

#[inline]
const fn block_index(x: usize, y: usize, z: usize) -> usize {
    (y * SECTION_SIZE + z) * SECTION_SIZE + x
}

fn plains_biome_id() -> u16 {
    static PLAINS_ID: OnceLock<u16> = OnceLock::new();
    *PLAINS_ID.get_or_init(|| {
        registries::BIOME
            .by_key(&Key::const_vanilla("plains"))
            .expect("vanilla biome registry should contain plains")
            .id()
    })
}

pub fn new_section() -> ChunkSection {
    let mut section = ChunkSection::new();
    let mut biomes = Palette::biomes();
    biomes.fill(plains_biome_id());
    section.set_biomes(biomes);
    section
}

#[derive(Debug)]
pub struct Sections {
    pub sections: Box<[RwLock<ChunkSection>]>,
}

const fn split_by_section(relative_y: usize) -> (usize, usize) {
    (relative_y / SECTION_SIZE, relative_y % SECTION_SIZE)
}

impl Sections {
    pub fn from_owned(sections: Box<[ChunkSection]>) -> Self {
        let sections = sections.into_vec().into_iter().map(RwLock::new).collect();
        Self { sections }
    }

    pub fn get_relative_block(
        &self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
    ) -> Option<BlockState> {
        debug_assert!(relative_x < SECTION_SIZE);
        debug_assert!(relative_z < SECTION_SIZE);

        let (section_index, relative_y) = split_by_section(relative_y);
        self.sections.get(section_index).map(|section| {
            state_from_id(section.read().get_block(relative_x, relative_y, relative_z))
        })
    }

    pub fn read_column_into(&self, x: usize, z: usize, buf: &mut Vec<BlockState>) {
        debug_assert!(x < SECTION_SIZE);
        debug_assert!(z < SECTION_SIZE);

        let total = self.sections.len() * SECTION_SIZE;
        if buf.len() != total {
            buf.resize(total, Block::AIR.default_state());
        }
        for (i, section) in self.sections.iter().enumerate() {
            let guard = section.read();
            let base = i * SECTION_SIZE;
            for (y, slot) in buf[base..base + SECTION_SIZE].iter_mut().enumerate() {
                *slot = state_from_id(guard.get_block(x, y, z));
            }
        }
    }

    pub fn get_relative_biome(
        &self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
    ) -> Option<u16> {
        let (section_index, relative_y) = split_by_section(relative_y);
        self.sections.get(section_index).map(|section| {
            section
                .read()
                .get_biome(relative_x / 4, relative_y / 4, relative_z / 4)
        })
    }

    pub fn for_each_biome_palette_entry(&self, mut visitor: impl FnMut(u16)) {
        for section in &self.sections {
            for biome_id in section.read().biomes().distinct() {
                visitor(biome_id);
            }
        }
    }

    pub fn recalculate_counts(&self) {
        for section in &self.sections {
            let mut guard = section.write();
            if guard.counts_are_stale() {
                guard.recalculate_counts();
            }
        }
    }

    fn write_grouped_by_section(
        &self,
        entries: usize,
        section_of: impl Fn(usize) -> usize,
        mut apply: impl FnMut(&mut [u16], usize),
    ) {
        let mut i = 0;
        while i < entries {
            let section_idx = section_of(i);
            let mut guard = self.sections[section_idx].write();
            let states = guard.block_states_mut();
            while i < entries && section_of(i) == section_idx {
                apply(states, i);
                i += 1;
            }
        }
    }

    pub fn write_column_blocks(&self, x: usize, z: usize, blocks: &[(usize, BlockState)]) {
        debug_assert!(x < SECTION_SIZE);
        debug_assert!(z < SECTION_SIZE);

        let xz_base = z * SECTION_SIZE + x;
        self.write_grouped_by_section(
            blocks.len(),
            |i| blocks[i].0 / SECTION_SIZE,
            |states, i| {
                let (rel_y, value) = blocks[i];
                states[(rel_y % SECTION_SIZE) * SECTION_SIZE * SECTION_SIZE + xz_base] =
                    value.state_id();
            },
        );
    }

    pub fn write_block_batch(&self, blocks: &[(usize, usize, usize, BlockState)]) {
        self.write_grouped_by_section(
            blocks.len(),
            |i| blocks[i].1 / SECTION_SIZE,
            |states, i| {
                let (x, rel_y, z, value) = blocks[i];
                states[block_index(x, rel_y % SECTION_SIZE, z)] = value.state_id();
            },
        );
    }

    pub fn set_relative_block(
        &self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
        value: BlockState,
    ) {
        debug_assert!(relative_x < SECTION_SIZE);
        debug_assert!(relative_z < SECTION_SIZE);

        let (idx, relative_y) = split_by_section(relative_y);
        self.sections[idx]
            .write()
            .set_block(relative_x, relative_y, relative_z, value.state_id());
    }
}

pub fn maybe_has(section: &ChunkSection, mut predicate: impl FnMut(BlockState) -> bool) -> bool {
    section
        .distinct_blocks()
        .any(|state_id| predicate(state_from_id(state_id)))
}

pub fn maybe_has_block_light_sources(section: &ChunkSection) -> bool {
    !section.is_empty() && maybe_has(section, |state| state.get_light_emission() > 0)
}

pub fn append_block_light_sources(
    section: &ChunkSection,
    chunk_min_x: i32,
    section_min_y: i32,
    chunk_min_z: i32,
    sources: &mut Vec<BlockPosition>,
) {
    if !maybe_has_block_light_sources(section) {
        return;
    }

    for local_index in 0..SECTION_VOLUME {
        let state = state_from_id(section.get_block_at_index(local_index));
        if state.get_light_emission() == 0 {
            continue;
        }

        sources.push(BlockPosition::new(
            chunk_min_x + (local_index & 15) as i32,
            section_min_y + (local_index >> 8) as i32,
            chunk_min_z + ((local_index >> 4) & 15) as i32,
        ));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::datapack;
    use crate::world::worldgen;

    fn init_test_registry() {
        use std::sync::Once;
        static INIT: Once = Once::new();
        INIT.call_once(|| {
            worldgen::ensure_registry_initialized(&datapack::Datapack::builtin())
                .expect("vanilla datapack should load");
        });
    }

    fn sections_of(count: usize) -> Sections {
        Sections::from_owned((0..count).map(|_| new_section()).collect())
    }

    #[test]
    fn a_new_section_defaults_to_plains_and_no_stored_states() {
        init_test_registry();

        let section = new_section();

        assert!(section.block_states().is_none());
        assert_eq!(section.get_biome(0, 0, 0), plains_biome_id());
    }

    #[test]
    fn generation_writes_then_recount_produce_the_vanilla_counts() {
        init_test_registry();

        let stone = Block::STONE.default_state();
        let water = Block::WATER.default_state();
        let lava = Block::LAVA.default_state();
        let sections = sections_of(1);

        sections.write_block_batch(&[
            (0, 0, 0, stone),
            (0, 1, 0, stone),
            (0, 2, 0, water),
            (0, 3, 0, water),
            (0, 4, 0, water),
            (0, 5, 0, lava),
        ]);
        sections.recalculate_counts();

        let section = sections.sections[0].read();
        assert_eq!(section.non_empty_block_count(), 6);
        assert_eq!(section.fluid_count(), 4);
    }

    #[test]
    fn column_writes_land_in_the_right_sections() {
        init_test_registry();

        let stone = Block::STONE.default_state();
        let sections = sections_of(2);

        sections.write_column_blocks(3, 5, &[(4, stone), (20, stone)]);

        assert_eq!(sections.get_relative_block(3, 4, 5), Some(stone));
        assert_eq!(sections.get_relative_block(3, 20, 5), Some(stone));
        assert_eq!(
            sections.get_relative_block(3, 6, 5),
            Some(Block::AIR.default_state())
        );
    }

    #[test]
    fn column_reads_match_point_reads() {
        init_test_registry();

        let stone = Block::STONE.default_state();
        let sections = sections_of(2);
        for y in 0..32 {
            sections.set_relative_block(11, y, 2, stone);
        }

        let mut column = Vec::new();
        sections.read_column_into(11, 2, &mut column);

        assert_eq!(column.len(), 32);
        for (y, state) in column.into_iter().enumerate() {
            assert_eq!(Some(state), sections.get_relative_block(11, y, 2));
        }
    }

    #[test]
    fn many_distinct_states_round_trip() {
        init_test_registry();

        let sections = sections_of(1);
        let cell = |i: u16| ((i % 16) as usize, (i / 16) as usize);

        for i in 1..256u16 {
            let (x, y) = cell(i);
            sections.set_relative_block(x, y, 0, state_from_id(i));
        }
        for i in 1..256u16 {
            let (x, y) = cell(i);
            assert_eq!(sections.get_relative_block(x, y, 0), Some(state_from_id(i)));
        }
    }
}