chunkgen 0.2.0

Minecraft world generation library
Documentation
mod biome_resolver;

use rustc_hash::FxHashSet;
use shulkr::util::Key;

use crate::registry::biome::BiomeRef;
use crate::world::worldgen::biome::climate::{ParameterList, Sampler, TargetPoint};
use shulkr::registry::registries;

pub use biome_resolver::BiomeResolver;

pub enum BiomeSource {
    MultiNoise(ParameterList<BiomeRef>),
    TheEnd([BiomeRef; 5]),
    Fixed(BiomeRef),
}

impl BiomeSource {
    pub const fn multi_noise(parameters: ParameterList<BiomeRef>) -> Self {
        Self::MultiNoise(parameters)
    }

    pub fn the_end() -> Self {
        Self::TheEnd(END_BIOME_PATHS.map(|path| {
            registries::BIOME
                .by_key(&Key::const_vanilla(path))
                .unwrap_or_else(|| panic!("missing end biome minecraft:{path}"))
        }))
    }

    const fn is_multi_noise(&self) -> bool {
        matches!(self, Self::MultiNoise(_))
    }

    pub fn possible_biomes(&self) -> Vec<BiomeRef> {
        match self {
            Self::MultiNoise(parameters) => distinct_biome_refs(
                parameters
                    .values()
                    .iter()
                    .map(|(_, biome)| *biome)
                    .collect(),
            ),
            Self::TheEnd(biomes) => biomes.to_vec(),
            Self::Fixed(biome) => vec![*biome],
        }
    }

    fn multi_noise_biome(&self, target: &TargetPoint, last_result: &mut Option<usize>) -> BiomeRef {
        match self {
            Self::MultiNoise(parameters) => parameters.find_value_cached(target, last_result),
            Self::TheEnd(_) | Self::Fixed(_) => {
                unreachable!("only multi-noise biome sources have climate parameters")
            }
        }
    }

    fn get_noise_biome(
        &self,
        sampler: &Sampler,
        last_result: &mut Option<usize>,
        quart_x: i32,
        quart_y: i32,
        quart_z: i32,
    ) -> BiomeRef {
        match self {
            Self::Fixed(biome) => biome,
            Self::MultiNoise(_) => {
                let target = sampler.sample(quart_x, quart_y, quart_z);
                self.multi_noise_biome(&target, last_result)
            }
            Self::TheEnd([end, highlands, midlands, islands, barrens]) => {
                let block_x = quart_x << 2;
                let block_y = quart_y << 2;
                let block_z = quart_z << 2;
                let chunk_x = block_x >> 4;
                let chunk_z = block_z >> 4;
                if i64::from(chunk_x) * i64::from(chunk_x) + i64::from(chunk_z) * i64::from(chunk_z)
                    <= 4096
                {
                    return end;
                }
                let weird_block_x = ((block_x >> 4) * 2 + 1) * 8;
                let weird_block_z = ((block_z >> 4) * 2 + 1) * 8;
                let height_value = f64::from(sampler.sample_value(
                    &sampler.erosion,
                    weird_block_x,
                    block_y,
                    weird_block_z,
                ));
                if height_value > 0.25 {
                    highlands
                } else if height_value >= -0.0625 {
                    midlands
                } else if height_value < -0.21875 {
                    islands
                } else {
                    barrens
                }
            }
        }
    }
}

fn distinct_biome_refs(biomes: Vec<BiomeRef>) -> Vec<BiomeRef> {
    let mut seen = FxHashSet::default();
    let mut distinct = Vec::with_capacity(biomes.len());

    for biome in biomes {
        if seen.insert(biome.key()) {
            distinct.push(biome);
        }
    }

    distinct
}

const END_BIOME_PATHS: [&str; 5] = [
    "the_end",
    "end_highlands",
    "end_midlands",
    "small_end_islands",
    "end_barrens",
];

#[cfg(test)]
#[expect(
    clippy::unwrap_used,
    reason = "test loads the checked-in vanilla datapack"
)]
mod tests {
    use crate::world::worldgen;
    use std::sync::Once;

    use super::*;
    use crate::datapack::Datapack;
    use crate::world::worldgen::biome::multi_noise::preset_parameter_list;

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

    #[test]
    fn overworld_and_nether_have_disjoint_biome_pools() {
        init_global_registry();
        let overworld = BiomeSource::multi_noise(
            preset_parameter_list(&Key::const_vanilla("overworld"))
                .unwrap()
                .unwrap(),
        );
        let nether = BiomeSource::multi_noise(
            preset_parameter_list(&Key::const_vanilla("nether"))
                .unwrap()
                .unwrap(),
        );
        let keys = |source: &BiomeSource| -> FxHashSet<Key> {
            source
                .possible_biomes()
                .iter()
                .map(|biome| biome.key().clone())
                .collect()
        };
        let overworld_biomes = keys(&overworld);
        let nether_biomes = keys(&nether);
        assert!(overworld_biomes.contains(&Key::const_vanilla("plains")));
        assert!(nether_biomes.contains(&Key::const_vanilla("nether_wastes")));
        assert!(!overworld_biomes.contains(&Key::const_vanilla("nether_wastes")));
    }

    #[test]
    fn the_end_has_five_biomes() {
        init_global_registry();
        assert_eq!(BiomeSource::the_end().possible_biomes().len(), 5);
    }
}