chunkgen 0.2.0

Minecraft world generation library
Documentation
use std::io::{Error as IoError, ErrorKind};

use serde::de::Error as DeError;
use serde_json::Value;
use shulkr::util::Key;
use shulkr::world::block::BlockState;

use crate::datapack::{Datapack, DatapackError, read};
use crate::world::worldgen::aquifer;
use crate::world::worldgen::biome::climate::Parameter;
use crate::world::worldgen::densityfunction::function::Df;
use crate::world::worldgen::densityfunction::registry::DensityRegistry;
use crate::world::worldgen::material::registry::MaterialRegistry;
use crate::world::worldgen::material::rule::MaterialRule;

const DIMENSION_Y_SIZE: i32 = 4064;
const DIMENSION_MAX_Y: i32 = (DIMENSION_Y_SIZE >> 1) - 1;
const DIMENSION_MIN_Y: i32 = DIMENSION_MAX_Y - DIMENSION_Y_SIZE + 1;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NoiseSettings {
    pub min_y: i32,
    pub height: i32,
}

impl NoiseSettings {
    pub const fn clamp_to_height_accessor(self, min_y: i32, max_y: i32) -> Self {
        let new_min_y = self.min_y.max(min_y);
        let new_height = (self.min_y + self.height).min(max_y + 1) - new_min_y;
        Self {
            min_y: new_min_y,
            height: new_height,
        }
    }
}

#[derive(Clone)]
pub struct NoiseRouter {
    pub temperature: Df,
    pub vegetation: Df,
    pub continents: Df,
    pub erosion: Df,
    pub depth: Df,
    pub ridges: Df,
    pub chunk_surface_level: Df,
    pub final_density: Df,
}

#[derive(Clone)]
pub struct SpawnTargetPoint {
    pub parameters: Vec<(Df, Parameter)>,
}

#[derive(Clone)]
pub struct NoiseGeneratorSettings {
    pub noise_settings: NoiseSettings,
    pub default_block: BlockState,
    pub default_fluid: BlockState,
    pub noise_router: NoiseRouter,
    pub material_rule: MaterialRule,
    pub spawn_target: Vec<SpawnTargetPoint>,
    pub sea_level: i32,
    pub disable_mob_generation: bool,
    pub aquifers: Option<aquifer::Config>,
    pub use_legacy_random_source: bool,
}

fn field<'a>(v: &'a Value, name: &str) -> Result<&'a Value, String> {
    v.get(name).ok_or_else(|| format!("missing field {name}"))
}

fn int(v: &Value, name: &str) -> Result<i32, String> {
    field(v, name)?
        .as_i64()
        .map(|n| n as i32)
        .ok_or_else(|| format!("{name} is not an integer"))
}

fn boolean(v: &Value, name: &str) -> Result<bool, String> {
    field(v, name)?
        .as_bool()
        .ok_or_else(|| format!("{name} is not a boolean"))
}

fn block_state(v: &Value, name: &str) -> Result<BlockState, String> {
    serde_json::from_value(field(v, name)?.clone()).map_err(|e| format!("{name}: {e}"))
}

fn float(v: &Value) -> Result<f32, String> {
    v.as_f64()
        .map(|n| n as f32)
        .ok_or_else(|| format!("{v} is not a number"))
}

pub(crate) fn parameter(v: &Value) -> Result<Parameter, String> {
    let (min, max) = if let Some(list) = v.as_array() {
        if list.len() != 2 {
            return Err(format!(
                "parameter range must have 2 elements, got {}",
                list.len()
            ));
        }
        (float(&list[0])?, float(&list[1])?)
    } else if v.is_object() {
        (float(field(v, "min")?)?, float(field(v, "max")?)?)
    } else {
        let point = float(v)?;
        (point, point)
    };
    for bound in [min, max] {
        if !(-2.0..=2.0).contains(&bound) {
            return Err(format!("parameter {bound} outside of range [-2.0:2.0]"));
        }
    }
    if min > max {
        return Err(format!(
            "Cannon construct interval, min > max ({min} > {max})"
        ));
    }
    Ok(Parameter::span(min, max))
}

impl NoiseSettings {
    fn parse(v: &Value) -> Result<Self, String> {
        let settings = Self {
            min_y: int(v, "min_y")?,
            height: int(v, "height")?,
        };
        if !(DIMENSION_MIN_Y..=DIMENSION_MAX_Y).contains(&settings.min_y) {
            return Err(format!(
                "min_y {} outside of range [{DIMENSION_MIN_Y}:{DIMENSION_MAX_Y}]",
                settings.min_y
            ));
        }
        if !(0..=DIMENSION_Y_SIZE).contains(&settings.height) {
            return Err(format!(
                "height {} outside of range [0:{DIMENSION_Y_SIZE}]",
                settings.height
            ));
        }
        if settings.min_y + settings.height > DIMENSION_MAX_Y + 1 {
            return Err(format!(
                "min_y + height cannot be higher than: {}",
                DIMENSION_MAX_Y + 1
            ));
        }
        if settings.height % 16 != 0 {
            return Err("height has to be a multiple of 16".to_owned());
        }
        if settings.min_y % 16 != 0 {
            return Err("min_y has to be a multiple of 16".to_owned());
        }
        Ok(settings)
    }
}

impl NoiseRouter {
    fn parse(registry: &DensityRegistry, v: &Value) -> Result<Self, String> {
        let f = |name: &str| registry.parse(field(v, name)?);
        Ok(Self {
            temperature: f("temperature")?,
            vegetation: f("vegetation")?,
            continents: f("continents")?,
            erosion: f("erosion")?,
            depth: f("depth")?,
            ridges: f("ridges")?,
            chunk_surface_level: f("chunk_surface_level")?,
            final_density: f("final_density")?,
        })
    }
}

impl SpawnTargetPoint {
    fn parse(registry: &DensityRegistry, v: &Value) -> Result<Self, String> {
        let map = v.as_object().ok_or("spawn target is not a map")?;
        let parameters = map
            .iter()
            .map(|(key, range)| Ok((registry.holder(key)?, parameter(range)?)))
            .collect::<Result<_, String>>()?;
        Ok(Self { parameters })
    }
}

impl NoiseGeneratorSettings {
    pub fn parse(
        registry: &DensityRegistry,
        materials: &MaterialRegistry,
        v: &Value,
    ) -> Result<Self, String> {
        let aquifers = v
            .get("aquifers")
            .map(|a| aquifer::Config::parse(registry, a))
            .transpose()?;
        let spawn_target = field(v, "spawn_target")?
            .as_array()
            .ok_or("spawn_target is not a list")?
            .iter()
            .map(|point| SpawnTargetPoint::parse(registry, point))
            .collect::<Result<_, String>>()?;
        Ok(Self {
            noise_settings: NoiseSettings::parse(field(v, "noise")?)?,
            default_block: block_state(v, "default_block")?,
            default_fluid: block_state(v, "default_fluid")?,
            noise_router: NoiseRouter::parse(registry, field(v, "noise_router")?)?,
            material_rule: materials.parse_rule(registry, field(v, "material_rule")?)?,
            spawn_target,
            sea_level: int(v, "sea_level")?,
            disable_mob_generation: boolean(v, "disable_mob_generation")?,
            aquifers,
            use_legacy_random_source: boolean(v, "legacy_random_source")?,
        })
    }

    pub fn load(
        pack: &Datapack,
        registry: &DensityRegistry,
        materials: &MaterialRegistry,
        id: &Key,
    ) -> Result<Self, DatapackError> {
        let relative = format!("worldgen/noise_settings/{}.json", id.path());
        let path =
            pack.files(id.namespace(), &relative)
                .pop()
                .ok_or_else(|| DatapackError::Read {
                    path: relative.clone().into(),
                    source: IoError::new(
                        ErrorKind::NotFound,
                        format!("{id} not present in the datapack"),
                    ),
                })?;
        let value: Value =
            serde_json::from_str(&read(&path)?).map_err(|source| DatapackError::Parse {
                path: path.clone(),
                source,
            })?;
        Self::parse(registry, materials, &value).map_err(|message| DatapackError::Parse {
            path,
            source: DeError::custom(message),
        })
    }
}