chunkgen 0.2.0

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

use crate::util::rng::RandomSource;
use crate::util::shuffle;
use crate::world::worldgen::block_predicate::HolderSet;
use crate::world::worldgen::feature::multiface::{self, Spreader};
use crate::world::worldgen::region::WorldGenRegion;
use shulkr::util::BlockPosition;
use shulkr::util::Direction;
use shulkr::world::block::BlockState;

#[derive(Debug, Deserialize)]
pub struct MultifaceGrowthConfig {
    pub block: Key,
    #[serde(default = "default_search_range")]
    pub search_range: i32,
    #[serde(default)]
    pub can_place_on_floor: bool,
    #[serde(default)]
    pub can_place_on_ceiling: bool,
    #[serde(default)]
    pub can_place_on_wall: bool,
    #[serde(default = "default_chance_of_spreading")]
    pub chance_of_spreading: f32,
    pub can_be_placed_on: HolderSet,
}

const fn default_search_range() -> i32 {
    10
}

const fn default_chance_of_spreading() -> f32 {
    0.5
}

fn is_air_or_water(state: BlockState) -> bool {
    state.is_air() || state.is(Block::WATER)
}

fn valid_directions(config: &MultifaceGrowthConfig) -> Vec<Direction> {
    let mut dirs = Vec::with_capacity(6);
    if config.can_place_on_ceiling {
        dirs.push(Direction::Up);
    }
    if config.can_place_on_floor {
        dirs.push(Direction::Down);
    }
    if config.can_place_on_wall {
        dirs.extend(Direction::HORIZONTAL);
    }
    dirs
}

fn shuffled_directions(
    config: &MultifaceGrowthConfig,
    random: &mut RandomSource,
) -> Vec<Direction> {
    let mut dirs = valid_directions(config);
    shuffle(&mut dirs, random);
    dirs
}

fn shuffled_directions_except(
    config: &MultifaceGrowthConfig,
    random: &mut RandomSource,
    except: Direction,
) -> Vec<Direction> {
    let mut dirs = valid_directions(config);
    dirs.retain(|&d| d != except);
    shuffle(&mut dirs, random);
    dirs
}

pub fn place(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    origin: BlockPosition,
    config: &MultifaceGrowthConfig,
) -> bool {
    if !is_air_or_water(region.get_block(origin)) {
        return false;
    }
    let Some(placer_block) = Block::from_key(config.block.clone()).copied() else {
        return false;
    };

    let search_dirs = shuffled_directions(config, random);
    if place_growth_if_possible(
        region,
        random,
        placer_block,
        origin,
        region.get_block(origin),
        config,
        &search_dirs,
    ) {
        return true;
    }

    for &search_dir in &search_dirs {
        let placement_dirs = shuffled_directions_except(config, random, search_dir.opposite());
        let pos = origin.relative(search_dir);
        for _ in 0..config.search_range {
            let state = region.get_block(pos);
            if !is_air_or_water(state) && !state.is(placer_block) {
                break;
            }
            if place_growth_if_possible(
                region,
                random,
                placer_block,
                pos,
                state,
                config,
                &placement_dirs,
            ) {
                return true;
            }
        }
    }

    false
}

fn place_growth_if_possible(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    placer_block: Block,
    pos: BlockPosition,
    old_state: BlockState,
    config: &MultifaceGrowthConfig,
    placement_dirs: &[Direction],
) -> bool {
    for &dir in placement_dirs {
        let neighbor = region.get_block(pos.relative(dir));
        if config.can_be_placed_on.contains_block(neighbor) {
            let Some(new_state) =
                multiface::state_for_placement(region, placer_block, old_state, pos, dir)
            else {
                return false;
            };
            region.set_block(pos, new_state);
            region.mark_pos_for_postprocessing(pos);
            if random.next_f32() < config.chance_of_spreading {
                Spreader::Default(placer_block).spread_from_face_toward_random_direction(
                    new_state, region, pos, dir, random, true,
                );
            }
            return true;
        }
    }
    false
}