chunkgen 0.2.0

Minecraft world generation library
Documentation
use serde::Deserialize;

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

#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(from = "RawFloatProvider")]
pub enum FloatProvider {
    Constant(f32),
    Uniform {
        min_inclusive: f32,
        max_exclusive: f32,
    },
    Trapezoid {
        min: f32,
        max: f32,
        plateau: f32,
    },
    ClampedNormal {
        mean: f32,
        deviation: f32,
        min: f32,
        max: f32,
    },
}

impl FloatProvider {
    pub fn sample(self, random: &mut RandomSource) -> f32 {
        match self {
            Self::Constant(v) => v,
            Self::Uniform {
                min_inclusive,
                max_exclusive,
            } => random.next_f32() * (max_exclusive - min_inclusive) + min_inclusive,
            Self::Trapezoid { min, max, plateau } => {
                let range = max - min;
                let plateau_start = (range - plateau) / 2.0;
                let plateau_end = range - plateau_start;
                min + random.next_f32() * plateau_end + random.next_f32() * plateau_start
            }
            Self::ClampedNormal {
                mean,
                deviation,
                min,
                max,
            } => {
                let sample = mean + deviation * random.next_gaussian() as f32;
                sample.clamp(min, max)
            }
        }
    }

    pub const fn min(self) -> f32 {
        match self {
            Self::Constant(v) => v,
            Self::Uniform { min_inclusive, .. } => min_inclusive,
            Self::Trapezoid { min, .. } | Self::ClampedNormal { min, .. } => min,
        }
    }

    pub const fn max(self) -> f32 {
        match self {
            Self::Constant(v) => v,
            Self::Uniform { max_exclusive, .. } => max_exclusive,
            Self::Trapezoid { max, .. } | Self::ClampedNormal { max, .. } => max,
        }
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
pub enum RawFloatProvider {
    Constant(f32),
    Tagged(TaggedFloatProvider),
}

#[derive(Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
pub enum TaggedFloatProvider {
    #[serde(rename = "minecraft:constant")]
    Constant { value: f32 },
    #[serde(rename = "minecraft:uniform")]
    Uniform {
        min_inclusive: f32,
        max_exclusive: f32,
    },
    #[serde(rename = "minecraft:trapezoid")]
    Trapezoid { min: f32, max: f32, plateau: f32 },
    #[serde(rename = "minecraft:clamped_normal")]
    ClampedNormal {
        mean: f32,
        deviation: f32,
        min: f32,
        max: f32,
    },
}

impl From<RawFloatProvider> for FloatProvider {
    fn from(raw: RawFloatProvider) -> Self {
        let tagged = match raw {
            RawFloatProvider::Constant(value) => return Self::Constant(value),
            RawFloatProvider::Tagged(tagged) => tagged,
        };
        match tagged {
            TaggedFloatProvider::Constant { value } => Self::Constant(value),
            TaggedFloatProvider::Uniform {
                min_inclusive,
                max_exclusive,
            } => Self::Uniform {
                min_inclusive,
                max_exclusive,
            },
            TaggedFloatProvider::Trapezoid { min, max, plateau } => {
                Self::Trapezoid { min, max, plateau }
            }
            TaggedFloatProvider::ClampedNormal {
                mean,
                deviation,
                min,
                max,
            } => Self::ClampedNormal {
                mean,
                deviation,
                min,
                max,
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::util::rng::xoroshiro::XoroshiroRandomSource;

    fn rng() -> RandomSource {
        RandomSource::Xoroshiro(XoroshiroRandomSource::from_seed(0))
    }

    #[test]
    fn constant_provider_ignores_the_rng() {
        let provider = FloatProvider::Constant(3.5);
        let mut random = rng();
        assert!((provider.sample(&mut random) - 3.5).abs() < f32::EPSILON);
        assert!((provider.min() - 3.5).abs() < f32::EPSILON);
        assert!((provider.max() - 3.5).abs() < f32::EPSILON);
    }

    #[test]
    fn uniform_provider_samples_within_bounds() {
        let provider = FloatProvider::Uniform {
            min_inclusive: -1.0,
            max_exclusive: 1.0,
        };
        let mut random = rng();
        for _ in 0..50 {
            let value = provider.sample(&mut random);
            assert!((-1.0..1.0).contains(&value));
        }
    }

    #[test]
    fn deserializes_bare_number_as_constant() {
        let provider: FloatProvider = serde_json::from_str("2.5").expect("test value");
        assert!(matches!(provider, FloatProvider::Constant(v) if (v - 2.5).abs() < f32::EPSILON));
    }
}