chunkgen 0.2.0

Minecraft world generation library
Documentation
use shulkr::world::block::{Block, BlockState, BoolProperty, Properties};

use crate::registry::fluid::is_water_fluid;
use shulkr::util::Direction;
use shulkr::util::is_offset_face_full;

use crate::util::rng::RandomSource;
use crate::util::shuffle;

use crate::world::worldgen::region::WorldGenRegion;
use shulkr::util::BlockPosition;

pub const DIRECTIONS: [Direction; 6] = Direction::ALL;

const fn face_property(direction: Direction) -> BoolProperty {
    match direction {
        Direction::Down => Properties::DOWN,
        Direction::Up => Properties::UP,
        Direction::North => Properties::NORTH,
        Direction::South => Properties::SOUTH,
        Direction::West => Properties::WEST,
        Direction::East => Properties::EAST,
    }
}

pub fn has_face(state: BlockState, direction: Direction) -> bool {
    state.get(face_property(direction)) == Some(true)
}

pub fn has_any_face(state: BlockState) -> bool {
    DIRECTIONS.iter().any(|&d| has_face(state, d))
}

pub fn available_faces(state: BlockState) -> Vec<Direction> {
    DIRECTIONS
        .into_iter()
        .filter(|&d| has_face(state, d))
        .collect()
}

pub fn set_face(state: BlockState, direction: Direction, value: bool) -> BlockState {
    state.with(face_property(direction), value)
}

pub fn can_attach_to(
    region: &WorldGenRegion<'_>,
    pos: BlockPosition,
    direction: Direction,
) -> bool {
    let neighbour_pos = pos.relative(direction);
    let neighbour = region.get_block(neighbour_pos);
    is_offset_face_full(
        neighbour.get_support_shape_at(neighbour_pos),
        direction.opposite(),
    ) || is_offset_face_full(
        neighbour.get_collision_shape_at(neighbour_pos),
        direction.opposite(),
    )
}

pub fn is_valid_state_for_placement(
    region: &WorldGenRegion<'_>,
    block: Block,
    old_state: BlockState,
    placement_pos: BlockPosition,
    placement_direction: Direction,
) -> bool {
    if old_state.as_block() == block && has_face(old_state, placement_direction) {
        return false;
    }
    can_attach_to(region, placement_pos, placement_direction)
}

pub fn state_for_placement(
    region: &WorldGenRegion<'_>,
    block: Block,
    old_state: BlockState,
    placement_pos: BlockPosition,
    placement_direction: Direction,
) -> Option<BlockState> {
    if !is_valid_state_for_placement(region, block, old_state, placement_pos, placement_direction) {
        return None;
    }

    let fluid = old_state.get_fluid_state();
    let new_state = if old_state.as_block() == block {
        old_state
    } else if fluid.is_source() && is_water_fluid(fluid.fluid()) {
        block.default_state().with(Properties::WATERLOGGED, true)
    } else {
        block.default_state()
    };

    Some(set_face(new_state, placement_direction, true))
}

#[derive(Clone, Copy)]
enum SpreadType {
    SamePosition,
    SamePlane,
    WrapAround,
}

const DEFAULT_SPREAD_ORDER: [SpreadType; 3] = [
    SpreadType::SamePosition,
    SpreadType::SamePlane,
    SpreadType::WrapAround,
];

impl SpreadType {
    fn spread_pos(
        self,
        pos: BlockPosition,
        spread_direction: Direction,
        from_face: Direction,
    ) -> (BlockPosition, Direction) {
        let relative = pos.relative(spread_direction);
        match self {
            Self::SamePosition => (pos, spread_direction),
            Self::SamePlane => (relative, from_face),
            Self::WrapAround => (relative.relative(from_face), spread_direction.opposite()),
        }
    }
}

#[derive(Clone, Copy)]
pub enum Spreader {
    Default(Block),
    SculkVein,
    SculkVeinSameSpace,
}

impl Spreader {
    const fn block(self) -> Block {
        match self {
            Self::Default(block) => block,
            Self::SculkVein | Self::SculkVeinSameSpace => Block::SCULK_VEIN,
        }
    }

    const fn spread_types(self) -> &'static [SpreadType] {
        match self {
            Self::SculkVeinSameSpace => &[SpreadType::SamePosition],
            Self::Default(_) | Self::SculkVein => &DEFAULT_SPREAD_ORDER,
        }
    }

    fn is_other_block_valid_as_source(self, state: BlockState) -> bool {
        match self {
            Self::Default(_) => false,
            Self::SculkVein | Self::SculkVeinSameSpace => !state.is(Block::SCULK_VEIN),
        }
    }

    fn can_spread_from(self, state: BlockState, face: Direction) -> bool {
        self.is_other_block_valid_as_source(state) || has_face(state, face)
    }

    fn state_can_be_replaced(
        self,
        region: &WorldGenRegion<'_>,
        source_pos: BlockPosition,
        placement_pos: BlockPosition,
        placement_direction: Direction,
        existing_state: BlockState,
    ) -> bool {
        if let Self::Default(block) = self {
            return existing_state.is_air()
                || existing_state.is(block)
                || (existing_state.is(Block::WATER)
                    && existing_state.get_fluid_state().is_source());
        }
        let against_state = region.get_block(placement_pos.relative(placement_direction));
        let against_block = against_state.as_block();

        if matches!(
            against_block,
            Block::SCULK | Block::SCULK_CATALYST | Block::MOVING_PISTON
        ) {
            return false;
        }

        let manhattan = (source_pos.x() - placement_pos.x()).abs()
            + (source_pos.y() - placement_pos.y()).abs()
            + (source_pos.z() - placement_pos.z()).abs();
        if manhattan == 2 {
            let neighbour_pos = source_pos.relative(placement_direction.opposite());
            if region
                .get_block(neighbour_pos)
                .is_side_solid_at(neighbour_pos, placement_direction)
            {
                return false;
            }
        }

        let fluid = existing_state.get_fluid_state();
        if !fluid.is_empty() && !is_water_fluid(fluid.fluid()) {
            return false;
        }

        let existing_block = existing_state.as_block();
        if matches!(existing_block, Block::FIRE | Block::SOUL_FIRE) {
            return false;
        }

        existing_state.is_replaceable()
            || existing_state.is_air()
            || existing_block == self.block()
            || (existing_block == Block::WATER && fluid.is_source())
    }

    fn can_spread_into(
        self,
        region: &WorldGenRegion<'_>,
        source_pos: BlockPosition,
        spread_pos: BlockPosition,
        spread_face: Direction,
    ) -> bool {
        let existing_state = region.get_block(spread_pos);
        self.state_can_be_replaced(region, source_pos, spread_pos, spread_face, existing_state)
            && is_valid_state_for_placement(
                region,
                self.block(),
                existing_state,
                spread_pos,
                spread_face,
            )
    }

    fn place_block(
        self,
        region: &WorldGenRegion<'_>,
        spread_pos: BlockPosition,
        spread_face: Direction,
        old_state: BlockState,
        post_process: bool,
    ) -> bool {
        match state_for_placement(region, self.block(), old_state, spread_pos, spread_face) {
            Some(new_state) => {
                if post_process {
                    region.mark_pos_for_postprocessing(spread_pos);
                }
                region.set_block(spread_pos, new_state)
            }
            None => false,
        }
    }

    fn get_spread_from_face_toward_direction(
        self,
        state: BlockState,
        region: &WorldGenRegion<'_>,
        pos: BlockPosition,
        from_face: Direction,
        spread_direction: Direction,
    ) -> Option<(BlockPosition, Direction)> {
        if spread_direction.axis() == from_face.axis() {
            return None;
        }

        if self.is_other_block_valid_as_source(state)
            || (has_face(state, from_face) && !has_face(state, spread_direction))
        {
            for &spread_type in self.spread_types() {
                let (spread_pos, spread_face) =
                    spread_type.spread_pos(pos, spread_direction, from_face);
                if self.can_spread_into(region, pos, spread_pos, spread_face) {
                    return Some((spread_pos, spread_face));
                }
            }
        }

        None
    }

    fn spread_from_face_toward_direction(
        self,
        state: BlockState,
        region: &WorldGenRegion<'_>,
        pos: BlockPosition,
        from_face: Direction,
        spread_direction: Direction,
        post_process: bool,
    ) -> bool {
        match self.get_spread_from_face_toward_direction(
            state,
            region,
            pos,
            from_face,
            spread_direction,
        ) {
            Some((spread_pos, spread_face)) => {
                let old_state = region.get_block(spread_pos);
                self.place_block(region, spread_pos, spread_face, old_state, post_process)
            }
            None => false,
        }
    }

    fn spread_from_face_toward_all_directions(
        self,
        state: BlockState,
        region: &WorldGenRegion<'_>,
        pos: BlockPosition,
        from_face: Direction,
        post_process: bool,
    ) -> i64 {
        DIRECTIONS
            .iter()
            .filter(|&&spread_direction| {
                self.spread_from_face_toward_direction(
                    state,
                    region,
                    pos,
                    from_face,
                    spread_direction,
                    post_process,
                )
            })
            .count() as i64
    }

    pub fn spread_all(
        self,
        state: BlockState,
        region: &WorldGenRegion<'_>,
        pos: BlockPosition,
        post_process: bool,
    ) -> i64 {
        DIRECTIONS
            .iter()
            .filter(|&&from_face| self.can_spread_from(state, from_face))
            .map(|&from_face| {
                self.spread_from_face_toward_all_directions(
                    state,
                    region,
                    pos,
                    from_face,
                    post_process,
                )
            })
            .sum()
    }

    pub fn spread_from_face_toward_random_direction(
        self,
        state: BlockState,
        region: &WorldGenRegion<'_>,
        pos: BlockPosition,
        starting_face: Direction,
        random: &mut RandomSource,
        post_process: bool,
    ) -> bool {
        let mut directions = DIRECTIONS;
        shuffle(&mut directions, random);
        directions.into_iter().any(|spread_direction| {
            self.spread_from_face_toward_direction(
                state,
                region,
                pos,
                starting_face,
                spread_direction,
                post_process,
            )
        })
    }
}