chunkgen 0.2.0

Minecraft world generation library
Documentation
use shulkr::util::BoundingBox;
use shulkr::util::Key;

use crate::registry::biome::cold_enough_to_snow;
use crate::util::rng::RandomSource;
use crate::util::rotation::RotationExt;
use crate::world::worldgen::generator::Generator;
use crate::world::worldgen::structure::structure::{GenerationContext, GenerationStub};
use crate::world::worldgen::structure::structures::ruined_portal_piece::{
    PortalProperties, RuinedPortalPiece, VerticalPlacement,
};
use crate::world::worldgen::structure::templatesystem::structure_template::bounding_box_of;
use crate::world::worldgen::structure::templatesystem::structure_template_manager::templates;
use shulkr::util::BlockPosition;
use shulkr::world::block::Mirror;
use shulkr::world::block::Rotation;

const STRUCTURE_LOCATION_PORTALS: &[&str] = &[
    "ruined_portal/portal_1",
    "ruined_portal/portal_2",
    "ruined_portal/portal_3",
    "ruined_portal/portal_4",
    "ruined_portal/portal_5",
    "ruined_portal/portal_6",
    "ruined_portal/portal_7",
    "ruined_portal/portal_8",
    "ruined_portal/portal_9",
    "ruined_portal/portal_10",
];

const STRUCTURE_LOCATION_GIANT_PORTALS: &[&str] = &[
    "ruined_portal/giant_portal_1",
    "ruined_portal/giant_portal_2",
    "ruined_portal/giant_portal_3",
];

const MIN_Y_INDEX: i32 = 15;

#[derive(Debug, Clone, Copy)]
pub struct Setup {
    pub placement: VerticalPlacement,
    pub air_pocket_probability: f32,
    pub mossiness: f32,
    pub overgrown: bool,
    pub vines: bool,
    pub can_be_cold: bool,
    pub replace_with_blackstone: bool,
    pub weight: f32,
}

fn sample(random: &mut RandomSource, limit: f32) -> bool {
    if limit == 0.0 {
        false
    } else if limit == 1.0 {
        true
    } else {
        random.next_f32() < limit
    }
}

fn random_within_interval(random: &mut RandomSource, min_preferred: i32, max: i32) -> i32 {
    if min_preferred < max {
        random.next_i32_between(min_preferred, max)
    } else {
        max
    }
}

#[expect(clippy::too_many_arguments, reason = "mirrors vanilla findSuitableY")]
fn find_suitable_y(
    random: &mut RandomSource,
    generator: &Generator,
    vertical_placement: VerticalPlacement,
    air_pocket: bool,
    surface_y_at_center: i32,
    y_span: i32,
    bounding_box: BoundingBox,
    min_y: i32,
) -> i32 {
    let min_y = min_y + MIN_Y_INDEX;
    let new_y = match vertical_placement {
        VerticalPlacement::InNether => {
            if air_pocket {
                random.next_i32_between(32, 100)
            } else if random.next_f32() < 0.5 {
                random.next_i32_between(27, 29)
            } else {
                random.next_i32_between(29, 100)
            }
        }
        VerticalPlacement::InMountain => {
            random_within_interval(random, 70, surface_y_at_center - y_span)
        }
        VerticalPlacement::Underground => {
            random_within_interval(random, min_y, surface_y_at_center - y_span)
        }
        VerticalPlacement::PartlyBuried => {
            surface_y_at_center - y_span + random.next_i32_between(2, 8)
        }
        VerticalPlacement::OnLandSurface | VerticalPlacement::OnOceanFloor => surface_y_at_center,
    };

    let corners = [
        (bounding_box.min_x(), bounding_box.min_z()),
        (bounding_box.max_x(), bounding_box.min_z()),
        (bounding_box.min_x(), bounding_box.max_z()),
        (bounding_box.max_x(), bounding_box.max_z()),
    ];
    let columns: Vec<_> = corners
        .iter()
        .map(|(x, z)| generator.get_base_column(*x, *z))
        .collect();
    let heightmap = vertical_placement.heightmap();

    let mut projected_y = new_y;
    while projected_y > min_y {
        let mut corners_on_solid_ground = 0;
        for column in &columns {
            if heightmap.is_opaque(column.get_block(projected_y)) {
                corners_on_solid_ground += 1;
                if corners_on_solid_ground == 3 {
                    return projected_y;
                }
            }
        }
        projected_y -= 1;
    }

    projected_y
}

pub fn find_generation_point<'a>(
    cx: &'a GenerationContext<'a>,
    setups: &'a [Setup],
) -> Option<GenerationStub<'a>> {
    let chosen_setup = cx.with_random(|random| {
        if setups.len() > 1 {
            let total: f32 = setups.iter().map(|setup| setup.weight).sum();
            let mut pick = random.next_f32();
            for setup in setups {
                pick -= setup.weight / total;
                if pick < 0.0 {
                    return *setup;
                }
            }
            setups[setups.len() - 1]
        } else {
            setups[0]
        }
    });

    let air_pocket = cx.with_random(|random| sample(random, chosen_setup.air_pocket_probability));
    let template_location: Key = cx.with_random(|random| {
        let pool = if random.next_f32() < 0.05 {
            STRUCTURE_LOCATION_GIANT_PORTALS
        } else {
            STRUCTURE_LOCATION_PORTALS
        };
        let index = random.next_i32_bounded(pool.len() as i32) as usize;
        Key::of(format!("minecraft:{}", pool[index]))
    });

    let template = templates().get_or_create(&template_location);
    let rotation = cx.with_random(Rotation::get_random);
    let mirror = cx.with_random(|random| {
        if random.next_f32() < 0.5 {
            Mirror::None
        } else {
            Mirror::FrontBack
        }
    });
    let size = template.size();
    let pivot = BlockPosition::new(size.x / 2, 0, size.z / 2);
    let chunk_pos = cx.chunk_pos;
    let base_position = BlockPosition::new(chunk_pos.0.x * 16, 0, chunk_pos.0.y * 16);
    let bounding_box = bounding_box_of(base_position, rotation, pivot, mirror, size);
    let center = bounding_box.center();
    let surface_y =
        cx.generator
            .get_base_height(center.x(), center.z(), chosen_setup.placement.heightmap())
            - 1;
    let projected_y = cx.with_random(|random| {
        find_suitable_y(
            random,
            cx.generator,
            chosen_setup.placement,
            air_pocket,
            surface_y,
            bounding_box.y_span(),
            bounding_box,
            cx.min_y,
        )
    });
    let origin = BlockPosition::new(base_position.x(), projected_y, base_position.z());

    Some(GenerationStub {
        position: origin,
        build: Box::new(move |builder| {
            let cold = chosen_setup.can_be_cold && is_cold(cx, origin);
            let properties = PortalProperties {
                cold,
                mossiness: chosen_setup.mossiness,
                air_pocket,
                overgrown: chosen_setup.overgrown,
                vines: chosen_setup.vines,
                replace_with_blackstone: chosen_setup.replace_with_blackstone,
            };
            builder.add_piece(RuinedPortalPiece::new(
                origin,
                chosen_setup.placement,
                properties,
                &template_location,
                rotation,
                mirror,
                pivot,
            ));
        }),
    })
}

fn is_cold(cx: &GenerationContext<'_>, origin: BlockPosition) -> bool {
    let biome = cx.biome_resolver.borrow_mut().get_noise_biome(
        origin.x() >> 2,
        origin.y() >> 2,
        origin.z() >> 2,
    );
    cold_enough_to_snow(biome, origin, cx.generator.sea_level())
}