yog_cli/design/
worldgen.rs1use base64::Engine;
6use yog_worldgen::{Generator, Terrain, preview};
7
8#[derive(serde::Serialize)]
9pub struct Preview {
10 top: String,
12 slice: String,
14 slice_top: i32,
16 slice_bottom: i32,
17}
18
19fn png(pixels: &[u32], w: u32, h: u32) -> Result<String, String> {
20 let rgba: Vec<u8> = pixels.iter().flat_map(|&p| [(p >> 16) as u8, (p >> 8) as u8, p as u8, (p >> 24) as u8]).collect();
21 let mut out = Vec::new();
22 let mut enc = png::Encoder::new(&mut out, w, h);
23 enc.set_color(png::ColorType::Rgba);
24 enc.set_depth(png::BitDepth::Eight);
25 enc.set_compression(png::Compression::Fast);
26 let mut writer = enc.write_header().map_err(|e| e.to_string())?;
27 writer.write_image_data(&rgba).map_err(|e| e.to_string())?;
28 writer.finish().map_err(|e| e.to_string())?;
29 Ok(format!("data:image/png;base64,{}", base64::engine::general_purpose::STANDARD.encode(out)))
30}
31
32pub fn render(text: &str, size: u32) -> Result<Preview, String> {
34 let size = size.clamp(16, 1024);
35 let def = yog_data::text::parse_value(text).map_err(|e| format!("{}:{}: {}", e.at.line, e.at.col, e.message))?;
36 let terrain = def.get("terrain").ok_or("no `terrain` — this dimension's chunks come from a generator in code")?;
37 let terrain = yog_data::read_value::<Terrain>(terrain).map_err(|ps| {
38 ps.iter().map(|p| format!("terrain.{}: {}", p.path, p.message)).collect::<Vec<_>>().join("\n")
39 })?;
40 let int = |k: &str| def.get("dimension_type").and_then(|t| t.get(k)).and_then(|v| v.as_i64()).map(|v| v as i32);
41 let min_y = int("min_y").unwrap_or(-64);
42 let height = int("height").unwrap_or(384);
43 let g = Generator::new(terrain);
44 let half = size as i32 / 2;
45 let top = preview::top_down(&g, -half, -half, size, size, min_y);
46 let highest = (-half..half).map(|x| g.surface(x, 0)).max().unwrap_or(0);
48 let fluid = g.terrain.fluid.as_ref().map_or(i32::MIN, |f| f.level);
49 let slice_top = (highest.max(fluid) + 4).min(min_y + height - 1);
50 let slice_bottom = (slice_top - 160).max(min_y);
51 let slice = preview::slice(&g, -half, 0, size, slice_top, slice_bottom, min_y);
52 Ok(Preview {
53 top: png(&top, size, size)?,
54 slice: png(&slice, size, (slice_top - slice_bottom + 1) as u32)?,
55 slice_top,
56 slice_bottom,
57 })
58}
59
60#[cfg(test)]
61mod tests {
62 #[test]
63 fn previews_a_dimensions_terrain() {
64 let text = r#"{ kind: "dimension", id: "hills", dimension_type: { min_y: 0, height: 256 },
65 terrain: { seed: 1, base_height: 64, noise: [{ scale: 50, amplitude: 12 }], fill: "@block:minecraft:stone",
66 fluid: { block: "@block:minecraft:water", level: 62 } } }"#;
67 let p = super::render(text, 32).unwrap();
68 assert!(p.top.starts_with("data:image/png;base64,") && p.slice.starts_with("data:image/png;base64,"));
69 assert_eq!(p.slice_bottom, 0);
70 assert!(p.slice_top >= 66);
71 let e = super::render(r#"{ kind: "dimension", id: "x" }"#, 32).err().unwrap();
72 assert!(e.contains("no `terrain`"), "{e}");
73 }
74}