chunkgen 0.2.0

Minecraft world generation library
Documentation
use std::collections::HashSet;

use serde::Deserialize;
use shulkr::util::Key;

use crate::util::hash_set::JavaHashSet;
use shulkr::util::BoundingBox;
use shulkr::world::block::Properties;

use crate::util::rng::RandomSource;
use crate::world::worldgen::feature::featuresize::FeatureSize;
use crate::world::worldgen::feature::foliageplacers::{FoliagePlacer, FoliageSetter};
use crate::world::worldgen::feature::rootplacers::RootPlacer;
use crate::world::worldgen::feature::state_provider::ProviderHolder;
use crate::world::worldgen::feature::treedecorators::{TreeDecorator, TreeDecoratorContext};
use crate::world::worldgen::feature::trunkplacers::TrunkPlacer;
use crate::world::worldgen::region::WorldGenRegion;
use shulkr::util::BlockPosition;
use shulkr::world::block::BlockState;

const LEAVES: Key = Key::const_vanilla("leaves");
const REPLACEABLE_BY_TREES: Key = Key::const_vanilla("replaceable_by_trees");
const VINE: Key = Key::const_vanilla("vine");
const PREVENTS_NEARBY_LEAF_DECAY: Key = Key::const_vanilla("prevents_nearby_leaf_decay");

#[derive(Debug, Deserialize)]
pub struct TreeConfig {
    pub trunk_provider: ProviderHolder,
    pub trunk_placer: TrunkPlacer,
    pub foliage_provider: ProviderHolder,
    pub foliage_placer: FoliagePlacer,
    pub minimum_size: FeatureSize,
    #[serde(default)]
    pub root_placer: Option<RootPlacer>,
    pub decorators: Vec<TreeDecorator>,
    #[serde(default)]
    pub ignore_vines: bool,
    pub below_trunk_provider: ProviderHolder,
}

pub fn lowest_trunk_or_root_of_tree(context: &TreeDecoratorContext<'_, '_>) -> Vec<BlockPosition> {
    let mut positions = Vec::new();
    if context.roots.is_empty() {
        positions.extend(context.logs.iter().copied());
    } else if !context.logs.is_empty() && context.roots[0].y() == context.logs[0].y() {
        positions.extend(context.logs.iter().copied());
        positions.extend(context.roots.iter().copied());
    } else {
        positions.extend(context.roots.iter().copied());
    }
    positions
}

pub fn is_vine(region: &WorldGenRegion<'_>, pos: BlockPosition) -> bool {
    region.get_block(pos).as_block().key() == &VINE
}

pub fn is_air_or_leaves(region: &WorldGenRegion<'_>, pos: BlockPosition) -> bool {
    let state = region.get_block(pos);
    state.is_air() || state.as_block().has_tag(&LEAVES)
}

pub fn valid_tree_pos(region: &WorldGenRegion<'_>, pos: BlockPosition) -> bool {
    let state = region.get_block(pos);
    state.is_air() || state.as_block().has_tag(&REPLACEABLE_BY_TREES)
}

pub fn place(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    origin: BlockPosition,
    config: &TreeConfig,
) -> bool {
    let mut root_positions = JavaHashSet::new();
    let mut trunks = JavaHashSet::new();
    let mut foliage_setter = FoliageSetter::new();
    let mut decorations = JavaHashSet::new();

    let placed = do_place(
        region,
        random,
        origin,
        &mut root_positions,
        &mut trunks,
        &mut foliage_setter,
        config,
    );
    if !placed || (trunks.is_empty() && foliage_setter.positions.is_empty()) {
        return false;
    }

    if !config.decorators.is_empty() {
        let mut context = TreeDecoratorContext::new(
            region,
            random,
            &trunks,
            &foliage_setter.positions,
            &root_positions,
            &mut decorations,
        );
        for decorator in &config.decorators {
            decorator.place(&mut context);
        }
    }

    let mut all = root_positions
        .iter()
        .chain(trunks.iter())
        .chain(foliage_setter.positions.iter())
        .chain(decorations.iter());
    let Some(first) = all.next() else {
        return false;
    };
    let mut bounds = BoundingBox::from_point(first);
    for pos in all {
        bounds = bounds.encapsulate(pos);
    }

    update_leaves(region, bounds, &trunks, &decorations, &root_positions);
    true
}

fn do_place(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    origin: BlockPosition,
    root_positions: &mut JavaHashSet,
    trunks: &mut JavaHashSet,
    foliage_setter: &mut FoliageSetter,
    config: &TreeConfig,
) -> bool {
    let tree_height = config.trunk_placer.get_tree_height(random);
    let foliage_height = config.foliage_placer.foliage_height(random, tree_height);
    let trunk_height = tree_height - foliage_height;
    let leaf_radius = config.foliage_placer.foliage_radius(random, trunk_height);
    let trunk_origin = config.root_placer.as_ref().map_or(origin, |root_placer| {
        root_placer.get_trunk_origin(origin, random)
    });
    let min_y = origin.y().min(trunk_origin.y());
    let max_y = origin.y().max(trunk_origin.y()) + tree_height + 1;
    if min_y < region.min_y() + 1 || max_y > region.max_y() {
        return false;
    }

    let min_clipped_height = config.minimum_size.min_clipped_height();
    let clipped_tree_height = get_max_free_tree_height(region, tree_height, trunk_origin, config);
    if clipped_tree_height < tree_height
        && !min_clipped_height.is_some_and(|min| clipped_tree_height >= min)
    {
        return false;
    }

    if let Some(root_placer) = &config.root_placer {
        let mut root_setter = |pos: BlockPosition, state: BlockState| {
            root_positions.insert(pos);
            region.set_block(pos, state);
        };
        if !root_placer.place_roots(
            region,
            &mut root_setter,
            random,
            origin,
            trunk_origin,
            config,
        ) {
            return false;
        }
    }

    let attachments = {
        let mut trunk_setter = |pos: BlockPosition, state: BlockState| {
            trunks.insert(pos);
            region.set_block(pos, state);
        };
        config.trunk_placer.place_trunk(
            region,
            &mut trunk_setter,
            random,
            clipped_tree_height,
            trunk_origin,
            config,
        )
    };
    for attachment in &attachments {
        config.foliage_placer.create_foliage(
            region,
            foliage_setter,
            random,
            config,
            clipped_tree_height,
            attachment,
            foliage_height,
            leaf_radius,
        );
    }
    true
}

fn get_max_free_tree_height(
    region: &WorldGenRegion<'_>,
    max_tree_height: i32,
    tree_pos: BlockPosition,
    config: &TreeConfig,
) -> i32 {
    for y in 0..=max_tree_height + 1 {
        let r = config.minimum_size.get_size_at_height(max_tree_height, y);
        for x in -r..=r {
            for z in -r..=r {
                let pos = tree_pos.offset(x, y, z);
                if !config.trunk_placer.is_free(region, pos)
                    || (!config.ignore_vines && is_vine(region, pos))
                {
                    return y - 2;
                }
            }
        }
    }
    max_tree_height
}

fn update_leaves(
    region: &WorldGenRegion<'_>,
    bounds: BoundingBox,
    logs: &JavaHashSet,
    decorations: &JavaHashSet,
    root_positions: &JavaHashSet,
) {
    const MAX_DISTANCE: usize = 7;
    let mut shape: HashSet<BlockPosition> = HashSet::new();
    let mut to_check: Vec<JavaHashSet> = (0..MAX_DISTANCE).map(|_| JavaHashSet::new()).collect();

    for pos in decorations.iter().chain(root_positions.iter()) {
        if bounds.is_inside(pos) {
            shape.insert(pos);
        }
    }

    let mut smallest_distance = 0usize;
    for pos in logs.iter() {
        to_check[0].insert(pos);
    }

    loop {
        while smallest_distance >= MAX_DISTANCE || !to_check[smallest_distance].is_empty() {
            if smallest_distance >= MAX_DISTANCE {
                return;
            }
            let pos = to_check[smallest_distance]
                .remove_first()
                .expect("bucket is non-empty");
            if !bounds.is_inside(pos) {
                continue;
            }
            if smallest_distance != 0 {
                let state = region.get_block(pos);
                region.set_block(
                    pos,
                    state.with(Properties::DISTANCE, smallest_distance as u8),
                );
            }
            shape.insert(pos);

            for direction in super::DIRECTIONS {
                let neighbour = pos.relative(direction);
                if !bounds.is_inside(neighbour) || shape.contains(&neighbour) {
                    continue;
                }
                let state = region.get_block(neighbour);
                let distance = if state.as_block().has_tag(&PREVENTS_NEARBY_LEAF_DECAY) {
                    0
                } else {
                    let Some(value) = state.get(Properties::DISTANCE) else {
                        continue;
                    };
                    usize::from(value)
                };
                let new_distance = distance.min(smallest_distance + 1);
                if new_distance < MAX_DISTANCE {
                    to_check[new_distance].insert(neighbour);
                    smallest_distance = smallest_distance.min(new_distance);
                }
            }
        }
        smallest_distance += 1;
    }
}