1pub 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#[derive(Schema, Debug, Clone, PartialEq)]
37pub struct NoiseLayer {
38 #[data(range = "1..=100000")]
39 pub scale: f64,
40 pub amplitude: f64,
41 #[data(default = "one", range = "1..=16")]
43 pub octaves: u32,
44 #[data(default = "half", range = "0..=1")]
45 pub persistence: f64,
46}
47
48#[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#[derive(Schema, Debug, Clone, PartialEq)]
59pub struct Fluid {
60 #[data(with = "RefTo<Block>")]
61 pub block: String,
62 pub level: i32,
63}
64
65#[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#[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#[derive(Schema, Debug, Clone, PartialEq)]
92pub struct Terrain {
93 #[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 #[data(with = "RefTo<Block>")]
102 pub fill: String,
103 #[data(default)]
105 pub layers: Vec<SurfaceLayer>,
106 #[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#[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 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 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 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 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 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 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 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}