Skip to main content

yog_worldgen/
lib.rs

1//! yog-worldgen — terrain as data: a dimension's ground described in its
2//! `.ydef` (`terrain`), generated by the runtime chunk by chunk and drawn
3//! by Yog-IDLE's designer from the same code.
4//!
5//! ```json5
6//! terrain: {
7//!   seed: 1234,
8//!   base_height: 64,
9//!   noise: [ { scale: 160, amplitude: 28, octaves: 4 }, { scale: 24, amplitude: 3 } ],
10//!   fill: "@block:minecraft:stone",
11//!   layers: [ { block: "@block:minecraft:grass_block", depth: 1 }, { block: "@block:minecraft:dirt", depth: 3 } ],
12//!   bedrock: "@block:minecraft:bedrock",
13//!   fluid: { block: "@block:minecraft:water", level: 62 },
14//!   caves: { scale: 24, threshold: 0.55 },
15//!   ores: [ { block: "@block:minecraft:iron_ore", size: 8, per_chunk: 12, min: 0, max: 64 } ],
16//! }
17//! ```
18//!
19//! The height of the ground is `base_height` plus each noise layer
20//! (`amplitude` blocks up or down, features `scale` blocks wide); the
21//! column is `layers` from the top, then `fill`; above the ground up to
22//! the fluid's level, the fluid. This is the low-level generator's
23//! declarative side: a mod still writes any generator of its own in code
24//! (`Registry::register_chunk_generator`).
25
26pub mod noise;
27pub mod preview;
28
29use yog_data::Schema;
30use yog_data::kinds::Block;
31use yog_data::with::{Opt, RefTo};
32
33use noise::{Perlin, Rng, mix};
34
35/// A layer of height: `amplitude` blocks up or down, features `scale` blocks wide.
36#[derive(Schema, Debug, Clone, PartialEq)]
37pub struct NoiseLayer {
38    #[data(range = "1..=100000")]
39    pub scale: f64,
40    pub amplitude: f64,
41    /// Finer layers on top of it, each half as strong (`persistence`).
42    #[data(default = "one", range = "1..=16")]
43    pub octaves: u32,
44    #[data(default = "half", range = "0..=1")]
45    pub persistence: f64,
46}
47
48/// A layer of the ground's top: `depth` blocks of `block`.
49#[derive(Schema, Debug, Clone, PartialEq)]
50pub struct SurfaceLayer {
51    #[data(with = "RefTo<Block>")]
52    pub block: String,
53    #[data(range = "1..=256")]
54    pub depth: u32,
55}
56
57/// What fills the low ground up to `level` (a sea, lava lakes).
58#[derive(Schema, Debug, Clone, PartialEq)]
59pub struct Fluid {
60    #[data(with = "RefTo<Block>")]
61    pub block: String,
62    pub level: i32,
63}
64
65/// Caves: where 3D noise of features `scale` blocks wide is above
66/// `threshold` (0–1: the higher, the fewer), under the ground.
67#[derive(Schema, Debug, Clone, PartialEq)]
68pub struct Caves {
69    #[data(range = "1..=10000")]
70    pub scale: f64,
71    #[data(range = "0..=1")]
72    pub threshold: f64,
73}
74
75/// Veins of `block` in the ground (in `replaces`, the fill by default).
76#[derive(Schema, Debug, Clone, PartialEq)]
77pub struct TerrainOre {
78    #[data(with = "RefTo<Block>")]
79    pub block: String,
80    #[data(with = "Opt<RefTo<Block>>")]
81    pub replaces: Option<String>,
82    #[data(range = "1..=64")]
83    pub size: u32,
84    #[data(range = "0..=256")]
85    pub per_chunk: u32,
86    pub min: i32,
87    pub max: i32,
88}
89
90/// A dimension's ground.
91#[derive(Schema, Debug, Clone, PartialEq)]
92pub struct Terrain {
93    /// The same seed, the same ground.
94    #[data(default)]
95    pub seed: u64,
96    #[data(default = "sea_level")]
97    pub base_height: i32,
98    #[data(default)]
99    pub noise: Vec<NoiseLayer>,
100    /// What the ground is made of.
101    #[data(with = "RefTo<Block>")]
102    pub fill: String,
103    /// The ground's top, from the surface down.
104    #[data(default)]
105    pub layers: Vec<SurfaceLayer>,
106    /// The world's floor (one block at the bottom).
107    #[data(with = "Opt<RefTo<Block>>")]
108    pub bedrock: Option<String>,
109    pub fluid: Option<Fluid>,
110    pub caves: Option<Caves>,
111    #[data(default)]
112    pub ores: Vec<TerrainOre>,
113}
114
115fn one() -> u32 {
116    1
117}
118
119fn half() -> f64 {
120    0.5
121}
122
123fn sea_level() -> i32 {
124    64
125}
126
127/// A [`Terrain`] ready to generate: its noise built from its seed.
128#[derive(Debug, Clone)]
129pub struct Generator {
130    pub terrain: Terrain,
131    height: Vec<Perlin>,
132    caves: Perlin,
133}
134
135impl Generator {
136    pub fn new(terrain: Terrain) -> Generator {
137        let height = (0..terrain.noise.len())
138            .map(|i| Perlin::new(mix(terrain.seed, i as u64 + 1)))
139            .collect();
140        let caves = Perlin::new(mix(terrain.seed, 0xCA7E));
141        Generator {
142            terrain,
143            height,
144            caves,
145        }
146    }
147
148    /// The height of the ground's top block at `x`, `z`.
149    pub fn surface(&self, x: i32, z: i32) -> i32 {
150        let t = &self.terrain;
151        let mut h = t.base_height as f64;
152        for (layer, noise) in t.noise.iter().zip(&self.height) {
153            h += layer.amplitude
154                * noise.fbm2(
155                    x as f64 / layer.scale,
156                    z as f64 / layer.scale,
157                    layer.octaves,
158                    layer.persistence,
159                );
160        }
161        h.round() as i32
162    }
163
164    /// The block at `x`, `y`, `z` (`None`: air) with the ground at `surface`
165    /// and the world's bottom at `min_y` — before ores.
166    pub fn block_at(&self, x: i32, y: i32, z: i32, surface: i32, min_y: i32) -> Option<&str> {
167        let t = &self.terrain;
168        if y == min_y
169            && let Some(b) = &t.bedrock
170        {
171            return Some(b);
172        }
173        if y > surface {
174            return t
175                .fluid
176                .as_ref()
177                .filter(|f| y <= f.level)
178                .map(|f| f.block.as_str());
179        }
180        if let Some(c) = &t.caves
181            && y > min_y
182            && y < surface
183        {
184            let n = self
185                .caves
186                .get3(x as f64 / c.scale, y as f64 / c.scale, z as f64 / c.scale);
187            // Perlin is about -1..1; the threshold is over 0..1.
188            if (n + 1.0) / 2.0 > c.threshold {
189                return None;
190            }
191        }
192        let depth = (surface - y) as u32;
193        let mut top = 0;
194        for l in &t.layers {
195            top += l.depth;
196            if depth < top {
197                return Some(&l.block);
198            }
199        }
200        Some(&t.fill)
201    }
202
203    /// Generates chunk `cx`, `cz` of a world `height` blocks from `min_y`:
204    /// `set(x, y, z, block)` for every block that isn't air (`x`, `z` 0..16).
205    pub fn generate(
206        &self,
207        cx: i32,
208        cz: i32,
209        min_y: i32,
210        height: i32,
211        mut set: impl FnMut(i32, i32, i32, &str),
212    ) {
213        let top = min_y + height - 1;
214        let mut surfaces = [[0i32; 16]; 16];
215        for lx in 0..16 {
216            for lz in 0..16 {
217                let (x, z) = (cx * 16 + lx, cz * 16 + lz);
218                let s = self.surface(x, z).clamp(min_y, top);
219                surfaces[lx as usize][lz as usize] = s;
220                let up = self
221                    .terrain
222                    .fluid
223                    .as_ref()
224                    .map_or(s, |f| f.level.max(s))
225                    .min(top);
226                for y in min_y..=up {
227                    if let Some(b) = self.block_at(x, y, z, s, min_y) {
228                        set(lx, y, lz, b);
229                    }
230                }
231            }
232        }
233        // Ores: veins started at random in the chunk, kept inside it.
234        for (i, ore) in self.terrain.ores.iter().enumerate() {
235            let mut rng = Rng::new(mix(
236                mix(self.terrain.seed, i as u64 + 100),
237                ((cx as u64) << 32) ^ (cz as u32 as u64),
238            ));
239            let replaces = ore.replaces.as_deref().unwrap_or(&self.terrain.fill);
240            for _ in 0..ore.per_chunk {
241                let (lo, hi) = (ore.min.max(min_y), ore.max.min(top));
242                if hi < lo {
243                    break;
244                }
245                let (mut lx, mut y, mut lz) = (
246                    rng.below(16) as i32,
247                    lo + rng.below((hi - lo + 1) as u32) as i32,
248                    rng.below(16) as i32,
249                );
250                for _ in 0..ore.size {
251                    if (0..16).contains(&lx) && (0..16).contains(&lz) && y >= min_y && y <= top {
252                        let (x, z) = (cx * 16 + lx, cz * 16 + lz);
253                        let s = surfaces[lx as usize][lz as usize];
254                        if self.block_at(x, y, z, s, min_y) == Some(replaces) {
255                            set(lx, y, lz, &ore.block);
256                        }
257                    }
258                    match rng.below(6) {
259                        0 => lx += 1,
260                        1 => lx -= 1,
261                        2 => y += 1,
262                        3 => y -= 1,
263                        4 => lz += 1,
264                        _ => lz -= 1,
265                    }
266                }
267            }
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    pub(crate) fn hills() -> Terrain {
277        let text = r#"{ seed: 7, base_height: 64,
278            noise: [ { scale: 64, amplitude: 20, octaves: 3 } ],
279            fill: "@block:minecraft:stone",
280            layers: [ { block: "@block:minecraft:grass_block", depth: 1 }, { block: "@block:minecraft:dirt", depth: 3 } ],
281            bedrock: "@block:minecraft:bedrock",
282            fluid: { block: "@block:minecraft:water", level: 62 },
283            ores: [ { block: "@block:minecraft:iron_ore", size: 8, per_chunk: 20, min: 0, max: 60 } ] }"#;
284        yog_data::from_text::<Terrain>(text).unwrap().value
285    }
286
287    #[test]
288    fn a_column_is_layers_then_fill_with_water_over_low_ground() {
289        let g = Generator::new(hills());
290        let mut blocks = std::collections::HashMap::new();
291        g.generate(0, 0, 0, 128, |x, y, z, b| {
292            blocks.insert((x, y, z), b.to_string());
293        });
294        let s = g.surface(0, 0);
295        assert_eq!(
296            blocks.get(&(0, 0, 0)).map(String::as_str),
297            Some("minecraft:bedrock")
298        );
299        let top = blocks.get(&(0, s, 0)).map(String::as_str);
300        assert!(
301            matches!(top, Some("minecraft:grass_block" | "minecraft:iron_ore")),
302            "{top:?} at {s}"
303        );
304        assert!(
305            blocks
306                .get(&(0, s - 2, 0))
307                .is_some_and(|b| b == "minecraft:dirt" || b == "minecraft:iron_ore")
308        );
309        assert!(
310            blocks.values().any(|b| b == "minecraft:iron_ore"),
311            "ores in the stone"
312        );
313        // Somewhere in a 16×16 chunk of hills ±20 around 64 the ground is under 62: water there.
314        let low = (0..16)
315            .flat_map(|x| (0..16).map(move |z| (x, z)))
316            .find(|&(x, z)| g.surface(x, z) < 61);
317        if let Some((x, z)) = low {
318            assert_eq!(
319                blocks.get(&(x, 62, z)).map(String::as_str),
320                Some("minecraft:water")
321            );
322        }
323        // The same terrain, the same chunk.
324        let mut again = std::collections::HashMap::new();
325        Generator::new(hills()).generate(0, 0, 0, 128, |x, y, z, b| {
326            again.insert((x, y, z), b.to_string());
327        });
328        assert_eq!(again, blocks);
329    }
330}