chunkgen 0.2.0

Minecraft world generation library
Documentation
use serde::Deserialize;
use shulkr::world::block::Block;

use crate::util::rng::RandomSource;
use crate::util::rng::legacy_random::LegacyRandomSource;
use crate::util::value_providers::int::IntProvider;
use crate::world::worldgen::block_predicate::HolderSet;
use crate::world::worldgen::feature::features::DIRECTIONS;
use crate::world::worldgen::feature::features::blockpos_iter::between_closed;
use crate::world::worldgen::feature::state_provider::ProviderHolder;
use crate::world::worldgen::region::WorldGenRegion;
use crate::world::worldgen::synth::normal_noise::NormalNoise;
use shulkr::util::BlockPosition;
use shulkr::world::block::BlockState;
use shulkr::world::block::Properties;
use std::sync;
use std::sync::LazyLock;

#[derive(Debug, Deserialize)]
pub struct GeodeConfig {
    pub blocks: GeodeBlockSettings,
    pub layers: GeodeLayerSettings,
    pub crack: GeodeCrackSettings,
    #[serde(default = "default_use_potential_placements_chance")]
    pub use_potential_placements_chance: f64,
    #[serde(default)]
    pub use_alternate_layer0_chance: f64,
    #[serde(default = "default_placements_require_layer0_alternate")]
    pub placements_require_layer0_alternate: bool,
    #[serde(default = "default_outer_wall_distance")]
    pub outer_wall_distance: IntProvider,
    #[serde(default = "default_distribution_points")]
    pub distribution_points: IntProvider,
    #[serde(default = "default_point_offset")]
    pub point_offset: IntProvider,
    #[serde(default = "default_min_gen_offset")]
    pub min_gen_offset: i32,
    #[serde(default = "default_max_gen_offset")]
    pub max_gen_offset: i32,
    #[serde(default = "default_noise_multiplier")]
    pub noise_multiplier: f64,
    pub invalid_blocks_threshold: i32,
}

#[derive(Debug, Deserialize)]
pub struct GeodeBlockSettings {
    pub filling_provider: ProviderHolder,
    pub inner_layer_provider: ProviderHolder,
    pub alternate_inner_layer_provider: ProviderHolder,
    pub middle_layer_provider: ProviderHolder,
    pub outer_layer_provider: ProviderHolder,
    pub inner_placements: Vec<BlockState>,
    pub cannot_replace: HolderSet,
    pub invalid_blocks: HolderSet,
}

#[derive(Debug, Deserialize)]
pub struct GeodeLayerSettings {
    #[serde(default = "default_filling")]
    pub filling: f64,
    #[serde(default = "default_inner_layer")]
    pub inner_layer: f64,
    #[serde(default = "default_middle_layer")]
    pub middle_layer: f64,
    #[serde(default = "default_outer_layer")]
    pub outer_layer: f64,
}

#[derive(Debug, Deserialize)]
pub struct GeodeCrackSettings {
    #[serde(default = "default_generate_crack_chance")]
    pub generate_crack_chance: f64,
    #[serde(default = "default_base_crack_size")]
    pub base_crack_size: f64,
    #[serde(default = "default_crack_point_offset")]
    pub crack_point_offset: i32,
}

const fn default_use_potential_placements_chance() -> f64 {
    0.35
}

const fn default_placements_require_layer0_alternate() -> bool {
    true
}

const fn default_outer_wall_distance() -> IntProvider {
    IntProvider::Uniform {
        min_inclusive: 4,
        max_inclusive: 5,
    }
}

const fn default_distribution_points() -> IntProvider {
    IntProvider::Uniform {
        min_inclusive: 3,
        max_inclusive: 4,
    }
}

const fn default_point_offset() -> IntProvider {
    IntProvider::Uniform {
        min_inclusive: 1,
        max_inclusive: 2,
    }
}

const fn default_min_gen_offset() -> i32 {
    -16
}

const fn default_max_gen_offset() -> i32 {
    16
}

const fn default_noise_multiplier() -> f64 {
    0.05
}

const fn default_filling() -> f64 {
    1.7
}

const fn default_inner_layer() -> f64 {
    2.2
}

const fn default_middle_layer() -> f64 {
    3.2
}

const fn default_outer_layer() -> f64 {
    4.2
}

const fn default_generate_crack_chance() -> f64 {
    1.0
}

const fn default_base_crack_size() -> f64 {
    2.0
}

const fn default_crack_point_offset() -> i32 {
    2
}

fn noise_parameters() -> &'static NormalNoise {
    static NOISE_PARAMETERS: LazyLock<NormalNoise> =
        sync::LazyLock::new(|| NormalNoise::create_parity(-4, &[1.0]));
    &NOISE_PARAMETERS
}

fn dist_sqr(a: BlockPosition, b: BlockPosition) -> f64 {
    let dx = f64::from(a.x() - b.x());
    let dy = f64::from(a.y() - b.y());
    let dz = f64::from(a.z() - b.z());
    dx * dx + dy * dy + dz * dz
}

fn inv_sqrt(value: f64) -> f64 {
    1.0 / value.sqrt()
}

fn safe_set_block(
    region: &WorldGenRegion<'_>,
    pos: BlockPosition,
    state: BlockState,
    cannot_replace: &HolderSet,
) {
    if !cannot_replace.contains_block(region.get_block(pos)) {
        region.set_block(pos, state);
    }
}

fn can_cluster_grow_at_state(state: BlockState) -> bool {
    state.is_air() || (state.is(Block::WATER) && state.get_fluid_state().is_full())
}

pub fn place(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    origin: BlockPosition,
    config: &GeodeConfig,
) -> bool {
    let min_gen_offset = config.min_gen_offset;
    let max_gen_offset = config.max_gen_offset;
    let mut points = Vec::new();
    let num_points = config.distribution_points.sample(random);
    let mut noise_random =
        RandomSource::Legacy(LegacyRandomSource::from_seed(region.seed() as u64));
    let noise = noise_parameters().create(&mut noise_random);
    let mut crack_points = Vec::new();
    let crack_size_adjustment = f64::from(num_points) / f64::from(config.outer_wall_distance.max());

    let layers = &config.layers;
    let blocks = &config.blocks;
    let crack = &config.crack;

    let inner_air = 1.0 / layers.filling.sqrt();
    let innermost_block_layer = 1.0 / (layers.inner_layer + crack_size_adjustment).sqrt();
    let inner_crust = 1.0 / (layers.middle_layer + crack_size_adjustment).sqrt();
    let outer_crust = 1.0 / (layers.outer_layer + crack_size_adjustment).sqrt();
    let crack_size = 1.0
        / (crack.base_crack_size
            + random.next_f64() / 2.0
            + if num_points > 3 {
                crack_size_adjustment
            } else {
                0.0
            })
        .sqrt();
    let should_generate_crack = f64::from(random.next_f32()) < crack.generate_crack_chance;
    let mut num_invalid_points = 0;

    for _ in 0..num_points {
        let x = config.outer_wall_distance.sample(random);
        let y = config.outer_wall_distance.sample(random);
        let z = config.outer_wall_distance.sample(random);
        let pos = origin.offset(x, y, z);
        let state = region.get_block(pos);
        if state.is_air() || blocks.invalid_blocks.contains_block(state) {
            num_invalid_points += 1;
            if num_invalid_points > config.invalid_blocks_threshold {
                return false;
            }
        }

        points.push((pos, config.point_offset.sample(random)));
    }

    if should_generate_crack {
        let offset_index = random.next_i32_bounded(4);
        let crack_offset = num_points * 2 + 1;
        let (dx, dz) = match offset_index {
            0 => (crack_offset, 0),
            1 => (0, crack_offset),
            2 => (crack_offset, crack_offset),
            _ => (0, 0),
        };
        crack_points.push(origin.offset(dx, 7, dz));
        crack_points.push(origin.offset(dx, 5, dz));
        crack_points.push(origin.offset(dx, 1, dz));
    }

    let mut potential_crystal_placements = Vec::new();
    let cannot_replace = &blocks.cannot_replace;

    for point_inside in between_closed(
        origin.offset(min_gen_offset, min_gen_offset, min_gen_offset),
        origin.offset(max_gen_offset, max_gen_offset, max_gen_offset),
    ) {
        let noise_offset = f64::from(noise.get(
            f64::from(point_inside.x()),
            f64::from(point_inside.y()),
            f64::from(point_inside.z()),
        )) * config.noise_multiplier;
        let mut dist_sum_shell = 0.0;
        let mut dist_sum_crack = 0.0;

        for (point, offset) in &points {
            dist_sum_shell +=
                inv_sqrt(dist_sqr(point_inside, *point) + f64::from(*offset)) + noise_offset;
        }

        for point in &crack_points {
            dist_sum_crack +=
                inv_sqrt(dist_sqr(point_inside, *point) + f64::from(crack.crack_point_offset))
                    + noise_offset;
        }

        if dist_sum_shell < outer_crust {
            continue;
        }

        if should_generate_crack && dist_sum_crack >= crack_size && dist_sum_shell < inner_air {
            safe_set_block(
                region,
                point_inside,
                Block::AIR.default_state(),
                cannot_replace,
            );
        } else if dist_sum_shell >= inner_air {
            let state = blocks
                .filling_provider
                .get_state(region, random, point_inside);
            safe_set_block(region, point_inside, state, cannot_replace);
        } else if dist_sum_shell >= innermost_block_layer {
            let use_alternate_layer =
                f64::from(random.next_f32()) < config.use_alternate_layer0_chance;
            let state = if use_alternate_layer {
                blocks
                    .alternate_inner_layer_provider
                    .get_state(region, random, point_inside)
            } else {
                blocks
                    .inner_layer_provider
                    .get_state(region, random, point_inside)
            };
            safe_set_block(region, point_inside, state, cannot_replace);

            if (!config.placements_require_layer0_alternate || use_alternate_layer)
                && f64::from(random.next_f32()) < config.use_potential_placements_chance
            {
                potential_crystal_placements.push(point_inside);
            }
        } else if dist_sum_shell >= inner_crust {
            let state = blocks
                .middle_layer_provider
                .get_state(region, random, point_inside);
            safe_set_block(region, point_inside, state, cannot_replace);
        } else if dist_sum_shell >= outer_crust {
            let state = blocks
                .outer_layer_provider
                .get_state(region, random, point_inside);
            safe_set_block(region, point_inside, state, cannot_replace);
        }
    }

    let inner_placements = &blocks.inner_placements;
    for crystal_pos in potential_crystal_placements {
        if inner_placements.is_empty() {
            break;
        }

        let mut state =
            inner_placements[random.next_i32_bounded(inner_placements.len() as i32) as usize];

        for direction in DIRECTIONS {
            if state.get(Properties::FACING).is_some() {
                state = state.with(Properties::FACING, direction);
            }

            let place_pos = crystal_pos.relative(direction);
            let place_state = region.get_block(place_pos);
            if state.get(Properties::WATERLOGGED).is_some() {
                state = state.with(
                    Properties::WATERLOGGED,
                    place_state.get_fluid_state().is_source(),
                );
            }

            if can_cluster_grow_at_state(place_state) {
                safe_set_block(region, place_pos, state, cannot_replace);
                break;
            }
        }
    }

    true
}