chunkgen 0.2.0

Minecraft world generation library
Documentation
use serde::Deserialize;

use crate::util::rng::RandomSource;
use crate::world::worldgen::worldgen_context::WorldGenContext;

use super::VerticalAnchor;

#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(from = "RawHeightProvider")]
pub enum HeightProvider {
    Constant(VerticalAnchor),
    Uniform {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
    },
    Trapezoid {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
        plateau: i32,
    },
    BiasedToBottom {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
        inner: i32,
    },
    VeryBiasedToBottom {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
        inner: i32,
    },
}

impl HeightProvider {
    pub fn sample(self, random: &mut RandomSource, context: &WorldGenContext) -> i32 {
        match self {
            Self::Constant(anchor) => anchor.resolve_y(context),
            Self::Uniform {
                min_inclusive,
                max_inclusive,
            } => {
                let min = min_inclusive.resolve_y(context);
                let max = max_inclusive.resolve_y(context);
                if min > max {
                    min
                } else {
                    random.next_i32_between(min, max)
                }
            }
            Self::Trapezoid {
                min_inclusive,
                max_inclusive,
                plateau,
            } => {
                let min = min_inclusive.resolve_y(context);
                let max = max_inclusive.resolve_y(context);
                if min > max {
                    min
                } else {
                    let range = max - min;
                    if plateau >= range {
                        random.next_i32_between(min, max)
                    } else {
                        let plateau_start = (range - plateau) / 2;
                        let plateau_end = range - plateau_start;
                        min + random.next_i32_between(0, plateau_end)
                            + random.next_i32_between(0, plateau_start)
                    }
                }
            }
            Self::BiasedToBottom {
                min_inclusive,
                max_inclusive,
                inner,
            } => {
                let min = min_inclusive.resolve_y(context);
                let max = max_inclusive.resolve_y(context);
                if max - min - inner < 0 {
                    min
                } else {
                    let limit = random.next_i32_bounded(max - min - inner + 1);
                    random.next_i32_bounded(limit + inner) + min
                }
            }
            Self::VeryBiasedToBottom {
                min_inclusive,
                max_inclusive,
                inner,
            } => {
                let min = min_inclusive.resolve_y(context);
                let max = max_inclusive.resolve_y(context);
                if max - min - inner < 0 {
                    min
                } else {
                    let upper_inclusive = random.next_i32_between(min + inner, max);
                    let biased_upper_inclusive = random.next_i32_between(min, upper_inclusive - 1);
                    random.next_i32_between(min, biased_upper_inclusive - 1 + inner)
                }
            }
        }
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
pub enum RawHeightProvider {
    Tagged(TaggedHeightProvider),
    Constant(VerticalAnchor),
}

#[derive(Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
pub enum TaggedHeightProvider {
    #[serde(rename = "minecraft:constant")]
    Constant { value: VerticalAnchor },
    #[serde(rename = "minecraft:uniform")]
    Uniform {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
    },
    #[serde(rename = "minecraft:trapezoid")]
    Trapezoid {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
        #[serde(default)]
        plateau: i32,
    },
    #[serde(rename = "minecraft:biased_to_bottom")]
    BiasedToBottom {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
        #[serde(default = "default_inner")]
        inner: i32,
    },
    #[serde(rename = "minecraft:very_biased_to_bottom")]
    VeryBiasedToBottom {
        min_inclusive: VerticalAnchor,
        max_inclusive: VerticalAnchor,
        #[serde(default = "default_inner")]
        inner: i32,
    },
}

const fn default_inner() -> i32 {
    1
}

impl From<RawHeightProvider> for HeightProvider {
    fn from(raw: RawHeightProvider) -> Self {
        let tagged = match raw {
            RawHeightProvider::Constant(anchor) => return Self::Constant(anchor),
            RawHeightProvider::Tagged(tagged) => tagged,
        };
        match tagged {
            TaggedHeightProvider::Constant { value } => Self::Constant(value),
            TaggedHeightProvider::Uniform {
                min_inclusive,
                max_inclusive,
            } => Self::Uniform {
                min_inclusive,
                max_inclusive,
            },
            TaggedHeightProvider::Trapezoid {
                min_inclusive,
                max_inclusive,
                plateau,
            } => Self::Trapezoid {
                min_inclusive,
                max_inclusive,
                plateau,
            },
            TaggedHeightProvider::BiasedToBottom {
                min_inclusive,
                max_inclusive,
                inner,
            } => Self::BiasedToBottom {
                min_inclusive,
                max_inclusive,
                inner,
            },
            TaggedHeightProvider::VeryBiasedToBottom {
                min_inclusive,
                max_inclusive,
                inner,
            } => Self::VeryBiasedToBottom {
                min_inclusive,
                max_inclusive,
                inner,
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const CONTEXT: WorldGenContext = WorldGenContext::new(-64, 384, 63, -64, 384);
    use crate::util::rng::xoroshiro::XoroshiroRandomSource;

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

    #[test]
    fn constant_provider_resolves_the_anchor() {
        let provider = HeightProvider::Constant(VerticalAnchor::Absolute(100));
        let mut random = rng();
        assert_eq!(provider.sample(&mut random, &CONTEXT), 100);
    }

    #[test]
    fn uniform_provider_samples_within_resolved_bounds() {
        let provider = HeightProvider::Uniform {
            min_inclusive: VerticalAnchor::AboveBottom(0),
            max_inclusive: VerticalAnchor::AboveBottom(10),
        };
        let mut random = rng();
        for _ in 0..50 {
            let value = provider.sample(&mut random, &CONTEXT);
            assert!((-64..=-54).contains(&value));
        }
    }

    #[test]
    fn inverted_range_falls_back_to_min() {
        let provider = HeightProvider::Uniform {
            min_inclusive: VerticalAnchor::AboveBottom(10),
            max_inclusive: VerticalAnchor::AboveBottom(0),
        };
        let mut random = rng();
        assert_eq!(provider.sample(&mut random, &CONTEXT), -54);
    }

    #[test]
    fn deserializes_bare_anchor_as_constant() {
        let provider: HeightProvider =
            serde_json::from_str(r#"{"absolute": 5}"#).expect("test value");
        assert!(matches!(
            provider,
            HeightProvider::Constant(VerticalAnchor::Absolute(5))
        ));
    }
}