diff --git a/src/bedrock_block_map.rs b/src/bedrock_block_map.rs index 395357c7f..289f7f081 100644 --- a/src/bedrock_block_map.rs +++ b/src/bedrock_block_map.rs @@ -229,6 +229,24 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock { ], ), + // Mangrove leaves with persistence (1.19+) + "mangrove_leaves" => BedrockBlock::with_states( + "mangrove_leaves", + vec![ + ("persistent_bit", BedrockBlockStateValue::Bool(true)), + ("update_bit", BedrockBlockStateValue::Bool(false)), + ], + ), + + // Azalea leaves with persistence (1.17+) + "azalea_leaves" => BedrockBlock::with_states( + "azalea_leaves", + vec![ + ("persistent_bit", BedrockBlockStateValue::Bool(true)), + ("update_bit", BedrockBlockStateValue::Bool(false)), + ], + ), + // Stone slab (bottom half by default) "stone_slab" => BedrockBlock::with_states( "stone_block_slab", diff --git a/src/biome.rs b/src/biome.rs new file mode 100644 index 000000000..46eee0a4f --- /dev/null +++ b/src/biome.rs @@ -0,0 +1,202 @@ +//! Land-cover-driven biome assignment for Java Anvil chunks (1.18+). + +use crate::coordinate_system::cartesian::XZPoint; +use crate::ground::Ground; +use crate::land_cover::{ + LC_BARE, LC_BUILT_UP, LC_CROPLAND, LC_GRASSLAND, LC_MANGROVES, LC_MOSS, LC_SHRUBLAND, + LC_SNOW_ICE, LC_TREE_COVER, LC_WATER, LC_WETLAND, +}; +use fastnbt::{LongArray, Value}; +use std::collections::HashMap; + +/// Map an ESA WorldCover class to a Minecraft biome ID. +pub fn biome_for_class(lc: u8, lat_deg: f64, water_dist: u8) -> &'static str { + let abs_lat = lat_deg.abs(); + match lc { + LC_TREE_COVER => { + if abs_lat > 55.0 { + "minecraft:taiga" + } else if abs_lat < 23.5 { + "minecraft:jungle" + } else { + "minecraft:forest" + } + } + LC_SHRUBLAND => "minecraft:savanna", + LC_GRASSLAND | LC_CROPLAND | LC_BUILT_UP => "minecraft:plains", + LC_BARE => "minecraft:desert", + LC_SNOW_ICE => "minecraft:snowy_plains", + LC_WATER => { + if water_dist >= 8 { + "minecraft:ocean" + } else { + "minecraft:river" + } + } + LC_WETLAND => "minecraft:swamp", + LC_MANGROVES => "minecraft:mangrove_swamp", + LC_MOSS => "minecraft:taiga", + _ => "minecraft:plains", + } +} + +pub type ChunkBiomeNbt = Value; + +/// Build the `biomes` compound for one chunk, sampling LC at a 4x4 grid +/// (4-block resolution) and packing into the Anvil 1.18+ palette+data layout. +pub fn build_chunk_biome_nbt( + chunk_x: i32, + chunk_z: i32, + ground: Option<&Ground>, + center_lat_deg: f64, +) -> ChunkBiomeNbt { + let mut names: [&'static str; 16] = ["minecraft:plains"; 16]; + + if let Some(g) = ground { + for zi in 0..4i32 { + for xi in 0..4i32 { + let world_x = chunk_x * 16 + xi * 4 + 2; + let world_z = chunk_z * 16 + zi * 4 + 2; + let coord = XZPoint::new(world_x, world_z); + let lc = g.cover_class(coord); + let wd = g.water_distance(coord); + names[(zi * 4 + xi) as usize] = biome_for_class(lc, center_lat_deg, wd); + } + } + } + + let mut palette: Vec<&'static str> = Vec::with_capacity(4); + let mut indices: [u8; 16] = [0; 16]; + for (i, &name) in names.iter().enumerate() { + let idx = match palette.iter().position(|p| *p == name) { + Some(idx) => idx, + None => { + palette.push(name); + palette.len() - 1 + } + }; + indices[i] = idx as u8; + } + + let palette_value = Value::List( + palette + .iter() + .map(|&s| Value::String(s.to_string())) + .collect(), + ); + + if palette.len() <= 1 { + let mut map = HashMap::with_capacity(1); + map.insert("palette".to_string(), palette_value); + return Value::Compound(map); + } + + let bits = bits_per_index(palette.len()); + let data = pack_biome_indices(&indices, bits); + + let mut map = HashMap::with_capacity(2); + map.insert("palette".to_string(), palette_value); + map.insert("data".to_string(), Value::LongArray(LongArray::new(data))); + Value::Compound(map) +} + +fn bits_per_index(palette_size: usize) -> u32 { + if palette_size <= 1 { + 0 + } else { + (palette_size - 1).ilog2() + 1 + } +} + +// Post-1.16 packing: values do not straddle long boundaries. +fn pack_biome_indices(indices_16: &[u8; 16], bits: u32) -> Vec { + debug_assert!((1..=6).contains(&bits)); + let bits = bits as usize; + let vals_per_long = 64 / bits; + let num_longs = 64usize.div_ceil(vals_per_long); + let mask: u64 = (1u64 << bits) - 1; + + let mut longs = vec![0u64; num_longs]; + for cell in 0..64usize { + // xz biomes repeat across y, so xz_idx = cell % 16. + let xz_idx = cell % 16; + let value = (indices_16[xz_idx] as u64) & mask; + let long_idx = cell / vals_per_long; + let bit_offset = (cell % vals_per_long) * bits; + longs[long_idx] |= value << bit_offset; + } + longs.into_iter().map(|u| u as i64).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bits_per_index_table() { + assert_eq!(bits_per_index(1), 0); + assert_eq!(bits_per_index(2), 1); + assert_eq!(bits_per_index(3), 2); + assert_eq!(bits_per_index(4), 2); + assert_eq!(bits_per_index(5), 3); + assert_eq!(bits_per_index(8), 3); + assert_eq!(bits_per_index(9), 4); + assert_eq!(bits_per_index(16), 4); + } + + #[test] + fn pack_alternating_1bit_fits_one_long() { + let mut indices = [0u8; 16]; + for (i, v) in indices.iter_mut().enumerate() { + *v = (i % 2) as u8; + } + let longs = pack_biome_indices(&indices, 1); + assert_eq!(longs.len(), 1); + let expected: u64 = (0..64u64).fold(0, |acc, c| acc | ((c % 2) << c)); + assert_eq!(longs[0] as u64, expected); + } + + #[test] + fn pack_three_biomes_uses_two_longs() { + let mut indices = [0u8; 16]; + for (i, v) in indices.iter_mut().enumerate() { + *v = (i % 3) as u8; + } + let longs = pack_biome_indices(&indices, 2); + assert_eq!(longs.len(), 2); + } + + #[test] + fn pack_three_bit_pads_to_four_longs() { + let indices = [4u8; 16]; + let longs = pack_biome_indices(&indices, 3); + assert_eq!(longs.len(), 4); + } + + #[test] + fn no_ground_yields_plains_palette() { + let nbt = build_chunk_biome_nbt(0, 0, None, 0.0); + match nbt { + Value::Compound(map) => { + assert!(map.contains_key("palette")); + assert!(!map.contains_key("data")); + } + _ => panic!("expected compound"), + } + } + + #[test] + fn latitude_drives_tree_biome() { + assert_eq!(biome_for_class(LC_TREE_COVER, 0.0, 0), "minecraft:jungle"); + assert_eq!(biome_for_class(LC_TREE_COVER, 40.0, 0), "minecraft:forest"); + assert_eq!(biome_for_class(LC_TREE_COVER, 60.0, 0), "minecraft:taiga"); + assert_eq!(biome_for_class(LC_TREE_COVER, -60.0, 0), "minecraft:taiga"); + } + + #[test] + fn water_distance_drives_river_vs_ocean() { + assert_eq!(biome_for_class(LC_WATER, 0.0, 1), "minecraft:river"); + assert_eq!(biome_for_class(LC_WATER, 0.0, 7), "minecraft:river"); + assert_eq!(biome_for_class(LC_WATER, 0.0, 8), "minecraft:ocean"); + } +} diff --git a/src/block_definitions.rs b/src/block_definitions.rs index 9b77ce5a9..aa3e709b5 100644 --- a/src/block_definitions.rs +++ b/src/block_definitions.rs @@ -106,7 +106,7 @@ impl Block { pub fn name(&self) -> &str { match self.id { - 0 => "acacia_planks", + 0 => "mangrove_log", 1 => "air", 2 => "andesite", 3 => "birch_leaves", @@ -329,8 +329,8 @@ impl Block { 230 => "cherry_log", 231 => "cherry_leaves", 232 => "brown_concrete_powder", - 233 => "orange_stained_glass", - 234 => "magenta_stained_glass", + 233 => "mangrove_leaves", + 234 => "azalea_leaves", 235 => "potted_poppy", 236 => "oak_trapdoor", 237 => "oak_trapdoor", @@ -345,9 +345,9 @@ impl Block { 246 => "potted_red_tulip", 247 => "potted_dandelion", 248 => "potted_blue_orchid", - 249 => "red_sand", - 250 => "red_sandstone", - 251 => "cactus", + 249 => "diamond_ore", + 250 => "redstone_ore", + 251 => "lapis_ore", 252 => "gray_concrete_powder", 253 => "cyan_terracotta", 254 => "black_wool", @@ -1035,6 +1035,14 @@ pub const CYAN_TERRACOTTA: Block = Block::new(253); pub const BLACK_WOOL: Block = Block::new(254); pub const LIGHT_GRAY_WALL_BANNER: Block = Block::new(255); +pub const MANGROVE_LOG: Block = Block::new(0); +pub const MANGROVE_LEAVES: Block = Block::new(233); +pub const AZALEA_LEAVES: Block = Block::new(234); + +pub const DIAMOND_ORE: Block = Block::new(249); +pub const REDSTONE_ORE: Block = Block::new(250); +pub const LAPIS_ORE: Block = Block::new(251); + /// Maps a block to a stair variant in the same colour family. #[inline] pub fn get_stair_block_for_material(material: Block) -> Block { diff --git a/src/data_processing.rs b/src/data_processing.rs index 8c1f5f4f9..8f85f1b17 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -417,6 +417,10 @@ pub fn generate_world_with_options( &building_footprints, )?; + if args.fillground { + crate::ore_generation::generate_ores(&mut editor, &xzbbox); + } + // Carve depth into ESA water cells (water_areas.rs only covers OSM polygons). crate::water_depth::carve_lc_water_pass( &mut editor, diff --git a/src/element_processing/landuse.rs b/src/element_processing/landuse.rs index b67c63be8..b9d916198 100644 --- a/src/element_processing/landuse.rs +++ b/src/element_processing/landuse.rs @@ -56,6 +56,7 @@ pub fn generate_landuse( // Get the area of the landuse element using cache let floor_area = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); + // Cherry/FloweringOak only via the random Tree::create pool (rare). let trees_ok_to_generate: Vec = { let mut trees: Vec = vec![]; if let Some(leaf_type) = element.tags.get("leaf_type") { @@ -63,18 +64,33 @@ pub fn generate_landuse( "broadleaved" => { trees.push(TreeType::Oak); trees.push(TreeType::Birch); + trees.push(TreeType::TallOak); + trees.push(TreeType::Bush); + trees.push(TreeType::AzaleaBush); + } + "needleleaved" => { + trees.push(TreeType::Spruce); + trees.push(TreeType::Pine); } - "needleleaved" => trees.push(TreeType::Spruce), _ => { trees.push(TreeType::Oak); trees.push(TreeType::Spruce); trees.push(TreeType::Birch); + trees.push(TreeType::TallOak); + trees.push(TreeType::Pine); + trees.push(TreeType::Bush); + trees.push(TreeType::AzaleaBush); + trees.push(TreeType::Willow); } } } else { trees.push(TreeType::Oak); trees.push(TreeType::Spruce); trees.push(TreeType::Birch); + trees.push(TreeType::TallOak); + trees.push(TreeType::Pine); + trees.push(TreeType::Bush); + trees.push(TreeType::AzaleaBush); } trees }; @@ -178,27 +194,32 @@ pub fn generate_landuse( } } "forest" if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) => { - let random_choice: i32 = rng.random_range(0..30); - if random_choice == 20 { + // Density-modulated spawn: thickets in some patches, clearings in others. + let density = crate::ground_generation::value_noise_01(x, z, 32); + let tree_threshold = ((60.0 - density * 45.0) as i32).max(5); + if rng.random_range(0..tree_threshold) == 0 { let tree_type = *trees_ok_to_generate .choose(&mut rng) .unwrap_or(&TreeType::Oak); Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints)); - } else if random_choice == 2 { - let flower_block: Block = match rng.random_range(1..=6) { - 1 => OAK_LEAVES, - 2 => RED_FLOWER, - 3 => BLUE_FLOWER, - 4 => YELLOW_FLOWER, - 5 => FERN, - _ => WHITE_FLOWER, - }; - editor.set_block(flower_block, x, 1, z, None, None); - } else if random_choice <= 12 { - if rng.random_range(0..100) < 12 { - editor.set_block(FERN, x, 1, z, None, None); - } else { - editor.set_block(GRASS, x, 1, z, None, None); + } else { + let random_choice: i32 = rng.random_range(0..30); + if random_choice == 2 { + let flower_block: Block = match rng.random_range(1..=6) { + 1 => OAK_LEAVES, + 2 => RED_FLOWER, + 3 => BLUE_FLOWER, + 4 => YELLOW_FLOWER, + 5 => FERN, + _ => WHITE_FLOWER, + }; + editor.set_block(flower_block, x, 1, z, None, None); + } else if random_choice <= 12 { + if rng.random_range(0..100) < 12 { + editor.set_block(FERN, x, 1, z, None, None); + } else { + editor.set_block(GRASS, x, 1, z, None, None); + } } } } diff --git a/src/element_processing/natural.rs b/src/element_processing/natural.rs index 361cdfbbb..fb7a44447 100644 --- a/src/element_processing/natural.rs +++ b/src/element_processing/natural.rs @@ -55,18 +55,25 @@ pub fn generate_natural( "broadleaved" => { trees_ok_to_generate.push(TreeType::Oak); trees_ok_to_generate.push(TreeType::Birch); + trees_ok_to_generate.push(TreeType::TallOak); + } + "needleleaved" => { + trees_ok_to_generate.push(TreeType::Spruce); + trees_ok_to_generate.push(TreeType::Pine); } - "needleleaved" => trees_ok_to_generate.push(TreeType::Spruce), _ => { trees_ok_to_generate.push(TreeType::Oak); trees_ok_to_generate.push(TreeType::Spruce); trees_ok_to_generate.push(TreeType::Birch); + trees_ok_to_generate.push(TreeType::TallOak); + trees_ok_to_generate.push(TreeType::Pine); } } } else { trees_ok_to_generate.push(TreeType::Oak); trees_ok_to_generate.push(TreeType::Spruce); trees_ok_to_generate.push(TreeType::Birch); + trees_ok_to_generate.push(TreeType::TallOak); } if trees_ok_to_generate.is_empty() { @@ -181,18 +188,31 @@ pub fn generate_natural( "broadleaved" => { trees.push(TreeType::Oak); trees.push(TreeType::Birch); + trees.push(TreeType::TallOak); + trees.push(TreeType::Bush); + trees.push(TreeType::AzaleaBush); + } + "needleleaved" => { + trees.push(TreeType::Spruce); + trees.push(TreeType::Pine); } - "needleleaved" => trees.push(TreeType::Spruce), _ => { trees.push(TreeType::Oak); trees.push(TreeType::Spruce); trees.push(TreeType::Birch); + trees.push(TreeType::TallOak); + trees.push(TreeType::Pine); + trees.push(TreeType::Bush); + trees.push(TreeType::AzaleaBush); } } } else { trees.push(TreeType::Oak); trees.push(TreeType::Spruce); trees.push(TreeType::Birch); + trees.push(TreeType::TallOak); + trees.push(TreeType::Bush); + trees.push(TreeType::AzaleaBush); } trees }; @@ -309,8 +329,11 @@ pub fn generate_natural( if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) { continue; } + let density = crate::ground_generation::value_noise_01(x, z, 32); + let tree_threshold = ((60.0 - density * 45.0) as i32).max(5); + let spawn_tree = rng.random_range(0..tree_threshold) == 0; let random_choice: i32 = rng.random_range(0..30); - if random_choice == 0 { + if spawn_tree { let tree_type = *trees_ok_to_generate .choose(&mut rng) .unwrap_or(&TreeType::Oak); @@ -372,12 +395,19 @@ pub fn generate_natural( editor.set_block(TALL_GRASS_TOP, x, 2, z, None, None); } "swamp" | "mangrove" => { - // TODO implement mangrove let random_choice: i32 = rng.random_range(0..40); if random_choice == 0 { - Tree::create( + let tree_type = if wetland_type == "mangrove" { + TreeType::Mangrove + } else if rng.random_bool(0.6) { + TreeType::Willow + } else { + TreeType::Mangrove + }; + Tree::create_of_type( editor, (x, 1, z), + tree_type, Some(building_footprints), ); } else if random_choice < 35 { diff --git a/src/element_processing/tree.rs b/src/element_processing/tree.rs index b366a2f6a..bca57f69f 100644 --- a/src/element_processing/tree.rs +++ b/src/element_processing/tree.rs @@ -6,9 +6,7 @@ use rand::Rng; type Coord = (i32, i32, i32); -// TODO all this data would probably be better suited in a TOML file or something. - -/// A circular pattern around a central point. +// Concentric rings added on top of the trunk column to bulk up the canopy. #[rustfmt::skip] const ROUND1_PATTERN: [Coord; 8] = [ (-2, 0, 0), @@ -21,7 +19,6 @@ const ROUND1_PATTERN: [Coord; 8] = [ (-1, 0, 1), ]; -/// A wider circular pattern. const ROUND2_PATTERN: [Coord; 12] = [ (3, 0, 0), (2, 0, -1), @@ -37,7 +34,6 @@ const ROUND2_PATTERN: [Coord; 12] = [ (0, 0, 3), ]; -/// A more scattered circular pattern. const ROUND3_PATTERN: [Coord; 12] = [ (3, 0, -1), (3, 0, 1), @@ -53,12 +49,10 @@ const ROUND3_PATTERN: [Coord; 12] = [ (-1, 0, -3), ]; -/// Used for iterating over each of the round patterns const ROUND_PATTERNS: [&[Coord]; 3] = [&ROUND1_PATTERN, &ROUND2_PATTERN, &ROUND3_PATTERN]; -////////////////////////////////////////////////// - -const OAK_LEAVES_FILL: [(Coord, Coord); 5] = [ +// Leaves-fill data per (species, variant): y-axis columns around the trunk. +const OAK_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 3, 0), (-1, 9, 0)), ((1, 3, 0), (1, 9, 0)), ((0, 3, -1), (0, 9, -1)), @@ -66,16 +60,57 @@ const OAK_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 9, 0), (0, 10, 0)), ]; -const SPRUCE_LEAVES_FILL: [(Coord, Coord); 6] = [ +const OAK_LEAVES_FILL_TALL_SLIM: [(Coord, Coord); 5] = [ + ((-1, 6, 0), (-1, 11, 0)), + ((1, 6, 0), (1, 11, 0)), + ((0, 6, -1), (0, 11, -1)), + ((0, 6, 1), (0, 11, 1)), + ((0, 11, 0), (0, 12, 0)), +]; + +const OAK_LEAVES_FILL_BUSHY: [(Coord, Coord); 5] = [ + ((-1, 3, 0), (-1, 7, 0)), + ((1, 3, 0), (1, 7, 0)), + ((0, 3, -1), (0, 7, -1)), + ((0, 3, 1), (0, 7, 1)), + ((0, 7, 0), (0, 8, 0)), +]; + +const OAK_LEAVES_FILL_COMPACT: [(Coord, Coord); 5] = [ + ((-1, 2, 0), (-1, 5, 0)), + ((1, 2, 0), (1, 5, 0)), + ((0, 2, -1), (0, 5, -1)), + ((0, 2, 1), (0, 5, 1)), + ((0, 5, 0), (0, 6, 0)), +]; + +// Spruce — three variants. Conifer cone shape with cross pattern. +const SPRUCE_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 3, 0), (-1, 10, 0)), ((0, 3, -1), (0, 10, -1)), ((1, 3, 0), (1, 10, 0)), - ((0, 3, -1), (0, 10, -1)), ((0, 3, 1), (0, 10, 1)), ((0, 11, 0), (0, 11, 0)), ]; -const BIRCH_LEAVES_FILL: [(Coord, Coord); 5] = [ +const SPRUCE_LEAVES_FILL_TOWERING: [(Coord, Coord); 5] = [ + ((-1, 4, 0), (-1, 13, 0)), + ((0, 4, -1), (0, 13, -1)), + ((1, 4, 0), (1, 13, 0)), + ((0, 4, 1), (0, 13, 1)), + ((0, 14, 0), (0, 14, 0)), +]; + +const SPRUCE_LEAVES_FILL_SQUAT: [(Coord, Coord); 5] = [ + ((-1, 2, 0), (-1, 7, 0)), + ((0, 2, -1), (0, 7, -1)), + ((1, 2, 0), (1, 7, 0)), + ((0, 2, 1), (0, 7, 1)), + ((0, 8, 0), (0, 8, 0)), +]; + +// Birch — three variants. Tall and slender by default. +const BIRCH_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 2, 0), (-1, 7, 0)), ((1, 2, 0), (1, 7, 0)), ((0, 2, -1), (0, 7, -1)), @@ -83,8 +118,24 @@ const BIRCH_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 7, 0), (0, 8, 0)), ]; -/// Dark oak: short but wide canopy, leaves start at y=3 up to y=6 with a cap -const DARK_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [ +const BIRCH_LEAVES_FILL_TALL: [(Coord, Coord); 5] = [ + ((-1, 5, 0), (-1, 10, 0)), + ((1, 5, 0), (1, 10, 0)), + ((0, 5, -1), (0, 10, -1)), + ((0, 5, 1), (0, 10, 1)), + ((0, 10, 0), (0, 11, 0)), +]; + +const BIRCH_LEAVES_FILL_CLUSTER: [(Coord, Coord); 5] = [ + ((-1, 3, 0), (-1, 5, 0)), + ((1, 3, 0), (1, 5, 0)), + ((0, 3, -1), (0, 5, -1)), + ((0, 3, 1), (0, 5, 1)), + ((0, 5, 0), (0, 6, 0)), +]; + +// Dark oak — three variants. Short trunk, wide canopy. +const DARK_OAK_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 3, 0), (-1, 6, 0)), ((1, 3, 0), (1, 6, 0)), ((0, 3, -1), (0, 6, -1)), @@ -92,8 +143,24 @@ const DARK_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 6, 0), (0, 7, 0)), ]; -/// Jungle: tall tree with canopy only near the top, leaves from y=7 to y=11 -const JUNGLE_LEAVES_FILL: [(Coord, Coord); 5] = [ +const DARK_OAK_LEAVES_FILL_TALL_BUSHY: [(Coord, Coord); 5] = [ + ((-1, 4, 0), (-1, 9, 0)), + ((1, 4, 0), (1, 9, 0)), + ((0, 4, -1), (0, 9, -1)), + ((0, 4, 1), (0, 9, 1)), + ((0, 9, 0), (0, 10, 0)), +]; + +const DARK_OAK_LEAVES_FILL_STUNTED: [(Coord, Coord); 5] = [ + ((-1, 2, 0), (-1, 4, 0)), + ((1, 2, 0), (1, 4, 0)), + ((0, 2, -1), (0, 4, -1)), + ((0, 2, 1), (0, 4, 1)), + ((0, 4, 0), (0, 5, 0)), +]; + +// Jungle — two variants. Tall trunk, canopy near top. +const JUNGLE_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 7, 0), (-1, 11, 0)), ((1, 7, 0), (1, 11, 0)), ((0, 7, -1), (0, 11, -1)), @@ -101,8 +168,16 @@ const JUNGLE_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 11, 0), (0, 12, 0)), ]; -/// Acacia: umbrella-shaped canopy with a gentle dome, leaves from y=5 to y=8 -const ACACIA_LEAVES_FILL: [(Coord, Coord); 5] = [ +const JUNGLE_LEAVES_FILL_BROAD: [(Coord, Coord); 5] = [ + ((-1, 8, 0), (-1, 12, 0)), + ((1, 8, 0), (1, 12, 0)), + ((0, 8, -1), (0, 12, -1)), + ((0, 8, 1), (0, 12, 1)), + ((0, 12, 0), (0, 13, 0)), +]; + +// Acacia — two variants. Umbrella canopy. +const ACACIA_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 5, 0), (-1, 8, 0)), ((1, 5, 0), (1, 8, 0)), ((0, 5, -1), (0, 8, -1)), @@ -110,8 +185,16 @@ const ACACIA_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 8, 0), (0, 9, 0)), ]; -/// Cherry: medium trunk with a wide, rounded canopy of pink blossoms. -const CHERRY_LEAVES_FILL: [(Coord, Coord); 5] = [ +const ACACIA_LEAVES_FILL_TALL: [(Coord, Coord); 5] = [ + ((-1, 7, 0), (-1, 10, 0)), + ((1, 7, 0), (1, 10, 0)), + ((0, 7, -1), (0, 10, -1)), + ((0, 7, 1), (0, 10, 1)), + ((0, 10, 0), (0, 10, 0)), +]; + +// Cherry — two variants. +const CHERRY_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 4, 0), (-1, 9, 0)), ((1, 4, 0), (1, 9, 0)), ((0, 4, -1), (0, 9, -1)), @@ -119,8 +202,16 @@ const CHERRY_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 9, 0), (0, 10, 0)), ]; -/// Tall oak: extra-tall trunk with a smaller crown sitting near the top. -const TALL_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [ +const CHERRY_LEAVES_FILL_WEEPING: [(Coord, Coord); 5] = [ + ((-1, 3, 0), (-1, 8, 0)), + ((1, 3, 0), (1, 8, 0)), + ((0, 3, -1), (0, 8, -1)), + ((0, 3, 1), (0, 8, 1)), + ((0, 8, 0), (0, 9, 0)), +]; + +// Tall oak — two variants. Extra-tall oak (kept for compatibility). +const TALL_OAK_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 8, 0), (-1, 12, 0)), ((1, 8, 0), (1, 12, 0)), ((0, 8, -1), (0, 12, -1)), @@ -128,31 +219,57 @@ const TALL_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [ ((0, 12, 0), (0, 13, 0)), ]; -/// Pine: very tall narrow conifer using spruce blocks. -const PINE_LEAVES_FILL: [(Coord, Coord); 6] = [ +const TALL_OAK_LEAVES_FILL_GIANT: [(Coord, Coord); 5] = [ + ((-1, 9, 0), (-1, 14, 0)), + ((1, 9, 0), (1, 14, 0)), + ((0, 9, -1), (0, 14, -1)), + ((0, 9, 1), (0, 14, 1)), + ((0, 14, 0), (0, 15, 0)), +]; + +// Pine — two variants. Narrow conifer using spruce blocks. +const PINE_LEAVES_FILL_STANDARD: [(Coord, Coord); 5] = [ ((-1, 5, 0), (-1, 12, 0)), ((0, 5, -1), (0, 12, -1)), ((1, 5, 0), (1, 12, 0)), - ((0, 5, -1), (0, 12, -1)), ((0, 5, 1), (0, 12, 1)), ((0, 13, 0), (0, 13, 0)), ]; -////////////////////////////////////////////////// +const PINE_LEAVES_FILL_TALL: [(Coord, Coord); 5] = [ + ((-1, 6, 0), (-1, 15, 0)), + ((0, 6, -1), (0, 15, -1)), + ((1, 6, 0), (1, 15, 0)), + ((0, 6, 1), (0, 15, 1)), + ((0, 16, 0), (0, 16, 0)), +]; -/// Maximum horizontal canopy radius used by the predefined tree patterns. -const MAX_CANOPY_RADIUS: i32 = 3; +// Mangrove — tall, narrow tropical/swamp tree. +const MANGROVE_LEAVES_FILL: [(Coord, Coord); 5] = [ + ((-1, 5, 0), (-1, 10, 0)), + ((1, 5, 0), (1, 10, 0)), + ((0, 5, -1), (0, 10, -1)), + ((0, 5, 1), (0, 10, 1)), + ((0, 10, 0), (0, 11, 0)), +]; -fn round_absolute( - editor: &mut WorldEditor, - material: Block, - (x, y, z): Coord, - block_pattern: &[Coord], -) { - for (i, j, k) in block_pattern { - editor.set_block_absolute(material, x + i, y + j, z + k, None, None); - } -} +// Bush — no real trunk; small leaf clump at ground level. +const BUSH_LEAVES_FILL: [(Coord, Coord); 3] = [ + ((-1, 0, -1), (1, 1, 1)), + ((-1, 2, 0), (1, 2, 0)), + ((0, 2, -1), (0, 2, 1)), +]; + +// Willow — short trunk, wide canopy. Drooping tendrils added separately. +const WILLOW_LEAVES_FILL: [(Coord, Coord); 5] = [ + ((-1, 4, 0), (-1, 7, 0)), + ((1, 4, 0), (1, 7, 0)), + ((0, 4, -1), (0, 7, -1)), + ((0, 4, 1), (0, 7, 1)), + ((0, 7, 0), (0, 8, 0)), +]; + +const MAX_CANOPY_RADIUS: i32 = 3; #[derive(Clone, Copy)] pub enum TreeType { @@ -165,19 +282,77 @@ pub enum TreeType { Cherry, TallOak, Pine, + Bush, + AzaleaBush, + Willow, + FloweringOak, + Mangrove, } -// TODO what should be moved in, and what should be referenced? -pub struct Tree<'a> { - // kind: TreeType, // NOTE: Not actually necessary to store! +pub struct Tree { log_block: Block, log_height: i32, leaves_block: Block, - leaves_fill: &'a [(Coord, Coord)], + leaves_fill: &'static [(Coord, Coord)], round_ranges: [Vec; 3], + branch_chance: f32, + accent_block: Option, + /// 0..100 percent chance per surface leaf to be the accent block. + accent_chance: u8, + drooping: bool, +} + +struct LeafPlacer<'a> { + leaves_block: Block, + accent_block: Option, + accent_chance: u8, + check_collision: bool, + footprints: Option<&'a BuildingFootprintBitmap>, +} + +impl LeafPlacer<'_> { + fn place_core(&self, editor: &mut WorldEditor, x: i32, y: i32, z: i32) { + self.place_with(editor, x, y, z, false); + } + + fn place_surface(&self, editor: &mut WorldEditor, x: i32, y: i32, z: i32) { + self.place_with(editor, x, y, z, true); + } + + fn place_with(&self, editor: &mut WorldEditor, x: i32, y: i32, z: i32, allow_accent: bool) { + if self.check_collision { + if let Some(fp) = self.footprints { + if fp.contains(x, z) { + return; + } + } + } + let h = (x as i64 as u64).wrapping_mul(73856093) + ^ (y as i64 as u64).wrapping_mul(19349663) + ^ (z as i64 as u64).wrapping_mul(83492791); + // ~4% organic leaf gap. + if h % 100 < 4 { + return; + } + let block = if allow_accent { + if let Some(accent) = self.accent_block { + let r2 = h.wrapping_mul(2654435761) % 100; + if r2 < self.accent_chance as u64 { + accent + } else { + self.leaves_block + } + } else { + self.leaves_block + } + } else { + self.leaves_block + }; + editor.set_block_absolute(block, x, y, z, None, None); + } } -impl Tree<'_> { +impl Tree { fn canopy_might_intersect_building( x: i32, z: i32, @@ -199,31 +374,28 @@ impl Tree<'_> { } /// Creates a tree at the specified coordinates. - /// - /// # Arguments - /// * `editor` - The world editor to place blocks - /// * `(x, y, z)` - The base coordinates for the tree - /// * `building_footprints` - Optional bitmap of (x, z) coordinates that are inside buildings. - /// If provided, trees will not be placed at coordinates within this bitmap. pub fn create( editor: &mut WorldEditor, (x, y, z): Coord, building_footprints: Option<&BuildingFootprintBitmap>, ) { - // Use deterministic RNG based on coordinates for consistent tree types across region boundaries - // The element_id of 0 is used as a salt for tree-specific randomness let mut rng = coord_rng(x, z, 0); - let tree_type = match rng.random_range(1..=50) { - 1..=13 => TreeType::Oak, - 14..=21 => TreeType::Spruce, - 22..=29 => TreeType::Birch, - 30..=33 => TreeType::DarkOak, - 34..=37 => TreeType::Jungle, - 38..=41 => TreeType::Acacia, - 42 => TreeType::Cherry, - 43..=46 => TreeType::TallOak, - 47..=50 => TreeType::Pine, + let tree_type = match rng.random_range(1..=100) { + 1..=20 => TreeType::Oak, + 21..=32 => TreeType::Spruce, + 33..=44 => TreeType::Birch, + 45..=50 => TreeType::DarkOak, + 51..=56 => TreeType::Jungle, + 57..=62 => TreeType::Acacia, + 63..=64 => TreeType::Cherry, + 65..=70 => TreeType::TallOak, + 71..=77 => TreeType::Pine, + 78..=84 => TreeType::Bush, + 85..=88 => TreeType::AzaleaBush, + 89..=92 => TreeType::Willow, + 93..=98 => TreeType::FloweringOak, + 99..=100 => TreeType::Mangrove, _ => unreachable!(), }; @@ -237,14 +409,13 @@ impl Tree<'_> { tree_type: TreeType, building_footprints: Option<&BuildingFootprintBitmap>, ) { - // Skip if this coordinate is inside a building if let Some(footprints) = building_footprints { if footprints.contains(x, z) { return; } } - // Skip if this coordinate is on a road, path, or other paved surface + // Skip road/path/water surfaces. if editor.check_for_block( x, 0, @@ -270,193 +441,594 @@ impl Tree<'_> { blacklist.extend(Self::get_functional_blocks()); blacklist.push(WATER); - let tree = Self::get_tree(tree_type); + // Salt 1 keeps the shape RNG independent of the type-pick RNG. + let mut shape_rng = coord_rng(x, z, 1); + let variant_idx: u32 = shape_rng.random(); + + let tree = Self::get_tree(tree_type, variant_idx); let check_canopy_collision = Self::canopy_might_intersect_building(x, z, building_footprints); - // Resolve trunk base Y once from the trunk's (x,z) position. - // All tree blocks use this as reference so the canopy doesn't - // warp to follow the hillside terrain. + // One base_y for the whole tree so the canopy doesn't warp to follow terrain. let base_y = editor.get_absolute_y(x, y, z); - // Build the logs - editor.fill_blocks_absolute( - tree.log_block, - x, - base_y, - z, - x, - base_y + tree.log_height, - z, - None, - Some(&blacklist), - ); + // Trunk jitter clamped so the canopy cap always sits above the trunk top. + let height_jitter = ((variant_idx >> 8) & 0x3) as i32 - 1; + let min_trunk = if tree.log_height == 0 { 0 } else { 2 }; + let canopy_top = tree + .leaves_fill + .iter() + .map(|((_, _, _), (_, j2, _))| *j2) + .max() + .unwrap_or(0); + let trunk_cap = (canopy_top - 1).max(min_trunk); + let trunk_height = (tree.log_height + height_jitter) + .max(min_trunk) + .min(trunk_cap); + + if tree.log_height > 0 { + editor.fill_blocks_absolute( + tree.log_block, + x, + base_y, + z, + x, + base_y + trunk_height, + z, + None, + Some(&blacklist), + ); + } - // Fill in the leaves + let placer = LeafPlacer { + leaves_block: tree.leaves_block, + accent_block: tree.accent_block, + accent_chance: tree.accent_chance, + check_collision: check_canopy_collision, + footprints: building_footprints, + }; + + // Inner canopy columns — accent only on the outermost ring (below). for ((i1, j1, k1), (i2, j2, k2)) in tree.leaves_fill { - if check_canopy_collision { - for leaf_x in (x + i1)..=(x + i2) { - for leaf_y in (base_y + j1)..=(base_y + j2) { - for leaf_z in (z + k1)..=(z + k2) { - if building_footprints.is_none_or(|fp| !fp.contains(leaf_x, leaf_z)) { - editor.set_block_absolute( - tree.leaves_block, - leaf_x, - leaf_y, - leaf_z, - None, - None, - ); - } - } + for leaf_x in (x + i1)..=(x + i2) { + for leaf_y in (base_y + j1)..=(base_y + j2) { + for leaf_z in (z + k1)..=(z + k2) { + placer.place_core(editor, leaf_x, leaf_y, leaf_z); } } - } else { - editor.fill_blocks_absolute( - tree.leaves_block, - x + i1, - base_y + j1, - z + k1, - x + i2, - base_y + j2, - z + k2, - None, - None, - ); } } - // Do the three rounds - for (round_range, round_pattern) in tree.round_ranges.iter().zip(ROUND_PATTERNS) { + // Only the outermost non-empty ring gets surface (accent-eligible) leaves. + let outermost_ring_idx: Option = tree + .round_ranges + .iter() + .enumerate() + .rev() + .find(|(_, r)| !r.is_empty()) + .map(|(i, _)| i); + for (idx, (round_range, round_pattern)) in + tree.round_ranges.iter().zip(ROUND_PATTERNS).enumerate() + { + let is_surface = Some(idx) == outermost_ring_idx; for offset in round_range { - if check_canopy_collision { - for &(i, j, k) in round_pattern { - let leaf_x = x + i; - let leaf_z = z + k; - if building_footprints.is_none_or(|fp| !fp.contains(leaf_x, leaf_z)) { - editor.set_block_absolute( - tree.leaves_block, - leaf_x, - base_y + offset + j, - leaf_z, - None, - None, - ); + for &(i, j, k) in round_pattern { + let lx = x + i; + let ly = base_y + offset + j; + let lz = z + k; + if is_surface { + placer.place_surface(editor, lx, ly, lz); + } else { + placer.place_core(editor, lx, ly, lz); + } + } + } + } + + let branch_roll = ((variant_idx >> 16) & 0xFF) as f32 / 255.0; + if branch_roll < tree.branch_chance && trunk_height >= 5 { + let (dx, dz) = match (variant_idx >> 24) & 0x3 { + 0 => (1, 0), + 1 => (-1, 0), + 2 => (0, 1), + _ => (0, -1), + }; + let branch_y_off = trunk_height - 2 - ((variant_idx >> 12) & 0x1) as i32; + let branch_y = base_y + branch_y_off; + for step in 1..=2 { + editor.set_block_absolute( + tree.log_block, + x + dx * step, + branch_y, + z + dz * step, + None, + Some(&blacklist), + ); + } + // Tapered cluster (Manhattan <= 2): rounder than a 3x3x3 cube. + let tip_x = x + dx * 2; + let tip_z = z + dz * 2; + for ddx in -1i32..=1 { + for ddy in -1i32..=1 { + for ddz in -1i32..=1 { + if ddx.abs() + ddy.abs() + ddz.abs() <= 2 { + placer.place_surface(editor, tip_x + ddx, branch_y + ddy, tip_z + ddz); } } - } else { - round_absolute( - editor, - tree.leaves_block, - (x, base_y + offset, z), - round_pattern, - ); + } + } + } + + if tree.drooping { + let droop_dirs: [(i32, i32); 8] = [ + (2, 0), + (-2, 0), + (0, 2), + (0, -2), + (1, 1), + (-1, -1), + (1, -1), + (-1, 1), + ]; + for (idx, &(dx, dz)) in droop_dirs.iter().enumerate() { + let len_bits = ((variant_idx >> (idx as u32 * 2)) & 0x3) as i32; + let droop_len = 2 + len_bits; + let top = base_y + 5; + for n in 0..droop_len { + placer.place_core(editor, x + dx, top - n, z + dz); } } } } - fn get_tree(kind: TreeType) -> Self { + fn get_tree(kind: TreeType, variant_idx: u32) -> Self { match kind { - TreeType::Oak => Self { - // kind, - log_block: OAK_LOG, - log_height: 8, - leaves_block: OAK_LEAVES, - leaves_fill: &OAK_LEAVES_FILL, - round_ranges: [ - (3..=8).rev().collect(), - (4..=7).rev().collect(), - (5..=6).rev().collect(), - ], + TreeType::Oak => match variant_idx % 5 { + 0 => Self::oak_standard(), + 1 => Self::oak_tall_slim(), + 2 => Self::oak_bushy(), + 3 => Self::oak_compact(), + _ => Self::oak_lopsided(), }, - - TreeType::Spruce => Self { - // kind, - log_block: SPRUCE_LOG, - log_height: 9, - leaves_block: SPRUCE_LEAVES, - leaves_fill: &SPRUCE_LEAVES_FILL, - // Conical shape: wide at bottom, narrow at top - round_ranges: [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]], + TreeType::Spruce => match variant_idx % 3 { + 0 => Self::spruce_standard(), + 1 => Self::spruce_towering(), + _ => Self::spruce_squat(), }, - - TreeType::Birch => Self { - // kind, - log_block: BIRCH_LOG, - log_height: 6, - leaves_block: BIRCH_LEAVES, - leaves_fill: &BIRCH_LEAVES_FILL, - round_ranges: [(2..=6).rev().collect(), (2..=4).collect(), vec![]], + TreeType::Birch => match variant_idx % 3 { + 0 => Self::birch_standard(), + 1 => Self::birch_tall(), + _ => Self::birch_cluster(), }, - - TreeType::DarkOak => Self { - // Short trunk with a very wide, bushy canopy - log_block: DARK_OAK_LOG, - log_height: 5, - leaves_block: DARK_OAK_LEAVES, - leaves_fill: &DARK_OAK_LEAVES_FILL, - // All 3 round patterns used for maximum width - round_ranges: [ - (3..=6).rev().collect(), - (3..=5).rev().collect(), - (4..=5).rev().collect(), - ], + TreeType::DarkOak => match variant_idx % 3 { + 0 => Self::dark_oak_standard(), + 1 => Self::dark_oak_tall_bushy(), + _ => Self::dark_oak_stunted(), }, - - TreeType::Jungle => Self { - // Tall trunk, canopy clustered at the top - log_block: JUNGLE_LOG, - log_height: 10, - leaves_block: JUNGLE_LEAVES, - leaves_fill: &JUNGLE_LEAVES_FILL, - // Canopy only near the top of the tree - round_ranges: [(7..=11).rev().collect(), (8..=10).rev().collect(), vec![]], + TreeType::Jungle => match variant_idx % 2 { + 0 => Self::jungle_standard(), + _ => Self::jungle_broad(), }, - - TreeType::Acacia => Self { - // Medium trunk with umbrella-shaped canopy, domed center - log_block: ACACIA_LOG, - log_height: 6, - leaves_block: ACACIA_LEAVES, - leaves_fill: &ACACIA_LEAVES_FILL, - // Inner rounds reach higher → gentle dome, outer stays low → wide brim - round_ranges: [ - (5..=8).rev().collect(), - (5..=7).rev().collect(), - (6..=7).rev().collect(), - ], + TreeType::Acacia => match variant_idx % 2 { + 0 => Self::acacia_standard(), + _ => Self::acacia_tall(), }, - - TreeType::Cherry => Self { - log_block: CHERRY_LOG, - log_height: 7, - leaves_block: CHERRY_LEAVES, - leaves_fill: &CHERRY_LEAVES_FILL, - round_ranges: [ - (4..=9).rev().collect(), - (5..=8).rev().collect(), - (6..=7).rev().collect(), - ], + TreeType::Cherry => match variant_idx % 2 { + 0 => Self::cherry_standard(), + _ => Self::cherry_weeping(), }, - - TreeType::TallOak => Self { - log_block: OAK_LOG, - log_height: 11, - leaves_block: OAK_LEAVES, - leaves_fill: &TALL_OAK_LEAVES_FILL, - round_ranges: [(8..=12).rev().collect(), (9..=11).rev().collect(), vec![10]], + TreeType::TallOak => match variant_idx % 2 { + 0 => Self::tall_oak_standard(), + _ => Self::tall_oak_giant(), }, - - TreeType::Pine => Self { - log_block: SPRUCE_LOG, - log_height: 12, - leaves_block: SPRUCE_LEAVES, - leaves_fill: &PINE_LEAVES_FILL, - round_ranges: [vec![11, 9, 7, 5], vec![8, 5], vec![]], + TreeType::Pine => match variant_idx % 2 { + 0 => Self::pine_standard(), + _ => Self::pine_tall(), }, - } // match - } // fn get_tree + TreeType::Bush => Self::bush(), + TreeType::AzaleaBush => Self::azalea_bush(), + TreeType::Willow => Self::willow(), + TreeType::FloweringOak => Self::flowering_oak(), + TreeType::Mangrove => Self::mangrove(), + } + } + + fn make( + log_block: Block, + log_height: i32, + leaves_block: Block, + leaves_fill: &'static [(Coord, Coord)], + round_ranges: [Vec; 3], + ) -> Self { + Self { + log_block, + log_height, + leaves_block, + leaves_fill, + round_ranges, + branch_chance: 0.0, + accent_block: None, + accent_chance: 0, + drooping: false, + } + } + + fn with_branch(mut self, chance: f32) -> Self { + self.branch_chance = chance; + self + } + + fn with_accent(mut self, block: Block, chance: u8) -> Self { + self.accent_block = Some(block); + self.accent_chance = chance; + self + } + + fn drooping(mut self) -> Self { + self.drooping = true; + self + } + + fn oak_standard() -> Self { + Self::make( + OAK_LOG, + 8, + OAK_LEAVES, + &OAK_LEAVES_FILL_STANDARD, + [ + (3..=8).rev().collect(), + (4..=7).rev().collect(), + (5..=6).rev().collect(), + ], + ) + .with_branch(0.30) + } + + fn oak_tall_slim() -> Self { + Self::make( + OAK_LOG, + 10, + OAK_LEAVES, + &OAK_LEAVES_FILL_TALL_SLIM, + [(7..=11).rev().collect(), (8..=10).rev().collect(), vec![]], + ) + .with_branch(0.40) + } + + fn oak_bushy() -> Self { + Self::make( + OAK_LOG, + 6, + OAK_LEAVES, + &OAK_LEAVES_FILL_BUSHY, + [ + (3..=7).rev().collect(), + (3..=6).rev().collect(), + (4..=5).rev().collect(), + ], + ) + .with_branch(0.20) + } + + fn oak_compact() -> Self { + Self::make( + OAK_LOG, + 5, + OAK_LEAVES, + &OAK_LEAVES_FILL_COMPACT, + [(2..=5).rev().collect(), (3..=4).rev().collect(), vec![]], + ) + } + + // Standard oak silhouette with a guaranteed side branch (the branch is the asymmetry). + fn oak_lopsided() -> Self { + Self::make( + OAK_LOG, + 8, + OAK_LEAVES, + &OAK_LEAVES_FILL_STANDARD, + [ + (3..=8).rev().collect(), + (4..=7).rev().collect(), + (5..=6).rev().collect(), + ], + ) + .with_branch(1.0) + } + + fn spruce_standard() -> Self { + Self::make( + SPRUCE_LOG, + 9, + SPRUCE_LEAVES, + &SPRUCE_LEAVES_FILL_STANDARD, + [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]], + ) + } + + fn spruce_towering() -> Self { + Self::make( + SPRUCE_LOG, + 12, + SPRUCE_LEAVES, + &SPRUCE_LEAVES_FILL_TOWERING, + [vec![12, 10, 8, 6, 4], vec![9, 6, 4], vec![]], + ) + } + + fn spruce_squat() -> Self { + Self::make( + SPRUCE_LOG, + 6, + SPRUCE_LEAVES, + &SPRUCE_LEAVES_FILL_SQUAT, + [vec![6, 4, 3], vec![4, 2], vec![3]], + ) + } + + fn birch_standard() -> Self { + Self::make( + BIRCH_LOG, + 6, + BIRCH_LEAVES, + &BIRCH_LEAVES_FILL_STANDARD, + [(2..=6).rev().collect(), (2..=4).collect(), vec![]], + ) + .with_branch(0.20) + } + + fn birch_tall() -> Self { + Self::make( + BIRCH_LOG, + 9, + BIRCH_LEAVES, + &BIRCH_LEAVES_FILL_TALL, + [(5..=9).rev().collect(), (6..=8).rev().collect(), vec![]], + ) + .with_branch(0.25) + } + + fn birch_cluster() -> Self { + Self::make( + BIRCH_LOG, + 4, + BIRCH_LEAVES, + &BIRCH_LEAVES_FILL_CLUSTER, + [(2..=4).rev().collect(), vec![3], vec![]], + ) + } + + fn dark_oak_standard() -> Self { + Self::make( + DARK_OAK_LOG, + 5, + DARK_OAK_LEAVES, + &DARK_OAK_LEAVES_FILL_STANDARD, + [ + (3..=6).rev().collect(), + (3..=5).rev().collect(), + (4..=5).rev().collect(), + ], + ) + .with_branch(0.40) + } + + fn dark_oak_tall_bushy() -> Self { + Self::make( + DARK_OAK_LOG, + 8, + DARK_OAK_LEAVES, + &DARK_OAK_LEAVES_FILL_TALL_BUSHY, + [ + (4..=9).rev().collect(), + (5..=8).rev().collect(), + (6..=7).rev().collect(), + ], + ) + .with_branch(0.50) + } + + fn dark_oak_stunted() -> Self { + Self::make( + DARK_OAK_LOG, + 3, + DARK_OAK_LEAVES, + &DARK_OAK_LEAVES_FILL_STUNTED, + [vec![3, 2], vec![2], vec![]], + ) + } + + fn jungle_standard() -> Self { + Self::make( + JUNGLE_LOG, + 10, + JUNGLE_LEAVES, + &JUNGLE_LEAVES_FILL_STANDARD, + [(7..=11).rev().collect(), (8..=10).rev().collect(), vec![]], + ) + .with_branch(0.50) + } + + fn jungle_broad() -> Self { + Self::make( + JUNGLE_LOG, + 11, + JUNGLE_LEAVES, + &JUNGLE_LEAVES_FILL_BROAD, + [ + (8..=12).rev().collect(), + (9..=11).rev().collect(), + (10..=10).rev().collect(), + ], + ) + .with_branch(0.60) + } + + fn acacia_standard() -> Self { + Self::make( + ACACIA_LOG, + 6, + ACACIA_LEAVES, + &ACACIA_LEAVES_FILL_STANDARD, + [ + (5..=8).rev().collect(), + (5..=7).rev().collect(), + (6..=7).rev().collect(), + ], + ) + .with_branch(0.35) + } + + fn acacia_tall() -> Self { + Self::make( + ACACIA_LOG, + 8, + ACACIA_LEAVES, + &ACACIA_LEAVES_FILL_TALL, + [(7..=10).rev().collect(), (8..=9).rev().collect(), vec![9]], + ) + .with_branch(0.45) + } + + fn cherry_standard() -> Self { + Self::make( + CHERRY_LOG, + 7, + CHERRY_LEAVES, + &CHERRY_LEAVES_FILL_STANDARD, + [ + (4..=9).rev().collect(), + (5..=8).rev().collect(), + (6..=7).rev().collect(), + ], + ) + .with_branch(0.30) + } + + fn cherry_weeping() -> Self { + Self::make( + CHERRY_LOG, + 6, + CHERRY_LEAVES, + &CHERRY_LEAVES_FILL_WEEPING, + [ + (3..=8).rev().collect(), + (4..=7).rev().collect(), + (5..=6).rev().collect(), + ], + ) + .drooping() + } + + fn tall_oak_standard() -> Self { + Self::make( + OAK_LOG, + 11, + OAK_LEAVES, + &TALL_OAK_LEAVES_FILL_STANDARD, + [(8..=12).rev().collect(), (9..=11).rev().collect(), vec![10]], + ) + .with_branch(0.40) + } + + fn tall_oak_giant() -> Self { + Self::make( + OAK_LOG, + 13, + OAK_LEAVES, + &TALL_OAK_LEAVES_FILL_GIANT, + [ + (9..=14).rev().collect(), + (10..=13).rev().collect(), + (11..=12).rev().collect(), + ], + ) + .with_branch(0.60) + } + + fn pine_standard() -> Self { + Self::make( + SPRUCE_LOG, + 12, + SPRUCE_LEAVES, + &PINE_LEAVES_FILL_STANDARD, + [vec![11, 9, 7, 5], vec![8, 5], vec![]], + ) + } + + fn pine_tall() -> Self { + Self::make( + SPRUCE_LOG, + 15, + SPRUCE_LEAVES, + &PINE_LEAVES_FILL_TALL, + [vec![14, 12, 10, 8, 6], vec![11, 7], vec![]], + ) + } + + // log_height == 0 → no trunk placed. + fn bush() -> Self { + Self::make( + OAK_LOG, + 0, + OAK_LEAVES, + &BUSH_LEAVES_FILL, + [vec![], vec![], vec![]], + ) + } + + fn azalea_bush() -> Self { + Self::make( + OAK_LOG, + 0, + AZALEA_LEAVES, + &BUSH_LEAVES_FILL, + [vec![], vec![], vec![]], + ) + } + + fn willow() -> Self { + Self::make( + OAK_LOG, + 5, + OAK_LEAVES, + &WILLOW_LEAVES_FILL, + [(4..=6).rev().collect(), (4..=5).rev().collect(), vec![5]], + ) + .drooping() + } + + // Oak silhouette with cherry-pink blossom accents on the outermost ring. + fn flowering_oak() -> Self { + Self::make( + OAK_LOG, + 8, + OAK_LEAVES, + &OAK_LEAVES_FILL_STANDARD, + [ + (3..=8).rev().collect(), + (4..=7).rev().collect(), + (5..=6).rev().collect(), + ], + ) + .with_branch(0.40) + .with_accent(CHERRY_LEAVES, 18) + } + + fn mangrove() -> Self { + Self::make( + MANGROVE_LOG, + 8, + MANGROVE_LEAVES, + &MANGROVE_LEAVES_FILL, + [ + (5..=10).rev().collect(), + (6..=9).rev().collect(), + (7..=8).rev().collect(), + ], + ) + .with_branch(0.55) + } /// Get all possible building wall blocks fn get_building_wall_blocks() -> Vec { @@ -606,4 +1178,4 @@ impl Tree<'_> { BEDROCK, ] } -} // impl Tree +} diff --git a/src/ground_generation.rs b/src/ground_generation.rs index 5d8cee71e..ba1f5c071 100644 --- a/src/ground_generation.rs +++ b/src/ground_generation.rs @@ -176,6 +176,44 @@ pub fn generate_ground_layer( ) }); + // --fillground fast path: bulk-fill fully-buried sections to + // Uniform(STONE) so the per-column loop only walks the boundary + // section. Gated on full bbox coverage so out-of-bbox columns + // don't get stone underneath. + let mut column_fill_y_min = crate::world_editor::MIN_Y + 1; + if args.fillground { + let chunk_fully_in_bbox = chunk_min_x == chunk_x << 4 + && chunk_max_x == (chunk_x << 4) + 15 + && chunk_min_z == chunk_z << 4 + && chunk_max_z == (chunk_z << 4) + 15; + let rotated_in = chunk_fully_in_bbox + && ground.is_in_rotated_bounds(chunk_min_x, chunk_min_z) + && ground.is_in_rotated_bounds(chunk_max_x, chunk_min_z) + && ground.is_in_rotated_bounds(chunk_min_x, chunk_max_z) + && ground.is_in_rotated_bounds(chunk_max_x, chunk_max_z); + if rotated_in { + let min_ground_y: i32 = if let Some(ref cache) = chunk_ground_cache { + cache + .grid + .iter() + .copied() + .min() + .unwrap_or(args.ground_level) + } else { + args.ground_level + }; + // section_top = section_y*16 + 15 <= min_ground_y - 3 + let top_buried = (min_ground_y - 18).div_euclid(16) as i8; + if top_buried >= crate::world_editor::MIN_SECTION_Y { + let all_clean = editor + .bulk_fill_chunk_sections_below(chunk_x, chunk_z, top_buried, STONE); + if all_clean { + column_fill_y_min = (top_buried as i32 + 1) * 16; + } + } + } + } + for x in chunk_min_x..=chunk_max_x { for z in chunk_min_z..=chunk_max_z { // Skip blocks outside the rotated original bounding box @@ -1072,13 +1110,13 @@ pub fn generate_ground_layer( } } - // Fill underground with stone + // Fill underground; column_fill_y_min skips already-Uniform sections. if args.fillground { editor.fill_column_absolute( STONE, x, z, - MIN_Y + 1, + column_fill_y_min, ground_y - 3, true, // skip_existing: don't overwrite blocks placed by element processing ); diff --git a/src/main.rs b/src/main.rs index 492025def..49e99d0b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod args; mod bedrock_block_map; +mod biome; mod block_definitions; mod bresenham; mod clipping; @@ -23,6 +24,7 @@ mod luanti_block_map; mod map_renderer; mod map_transformation; mod models_3d; +mod ore_generation; mod osm_parser; mod overture; #[cfg(feature = "gui")] diff --git a/src/ore_generation.rs b/src/ore_generation.rs new file mode 100644 index 000000000..7225c4db1 --- /dev/null +++ b/src/ore_generation.rs @@ -0,0 +1,135 @@ +//! Random ore veins for the stone produced by `--fillground`. + +use crate::block_definitions::{ + Block, COAL_ORE, DIAMOND_ORE, GOLD_ORE, IRON_ORE, LAPIS_ORE, REDSTONE_ORE, STONE, +}; +use crate::coordinate_system::cartesian::XZBBox; +use crate::deterministic_rng::coord_rng; +use crate::progress::emit_gui_progress_update; +use crate::world_editor::{WorldEditor, MIN_Y}; +use colored::Colorize; +use rand::Rng; + +struct OreDef { + block: Block, + /// Shallowest depth below local ground level (e.g. 3 = 3 blocks under surface). + depth_min: i32, + /// Deepest depth below local ground level. + depth_max: i32, + vein_min: u32, + vein_max: u32, + /// Sampled as uniform 0..=2*avg, giving the requested mean. + avg_veins_per_chunk: u32, +} + +const ORES: &[OreDef] = &[ + OreDef { + block: COAL_ORE, + depth_min: 3, + depth_max: 45, + vein_min: 8, + vein_max: 17, + avg_veins_per_chunk: 8, + }, + OreDef { + block: IRON_ORE, + depth_min: 3, + depth_max: 60, + vein_min: 5, + vein_max: 9, + avg_veins_per_chunk: 6, + }, + OreDef { + block: LAPIS_ORE, + depth_min: 25, + depth_max: 55, + vein_min: 4, + vein_max: 7, + avg_veins_per_chunk: 2, + }, + OreDef { + block: GOLD_ORE, + depth_min: 40, + depth_max: 60, + vein_min: 5, + vein_max: 9, + avg_veins_per_chunk: 3, + }, + OreDef { + block: REDSTONE_ORE, + depth_min: 45, + depth_max: 65, + vein_min: 5, + vein_max: 10, + avg_veins_per_chunk: 4, + }, + OreDef { + block: DIAMOND_ORE, + depth_min: 50, + depth_max: 65, + vein_min: 4, + vein_max: 7, + avg_veins_per_chunk: 1, + }, +]; + +/// Place ore veins across every chunk; Y is relative to local ground. +pub fn generate_ores(editor: &mut WorldEditor, xzbbox: &XZBBox) { + println!("{} Sprinkling ore veins...", "[6b/7]".bold()); + emit_gui_progress_update(89.0, "Sprinkling ore veins..."); + + let min_chunk_x = xzbbox.min_x() >> 4; + let max_chunk_x = xzbbox.max_x() >> 4; + let min_chunk_z = xzbbox.min_z() >> 4; + let max_chunk_z = xzbbox.max_z() >> 4; + + for chunk_x in min_chunk_x..=max_chunk_x { + for chunk_z in min_chunk_z..=max_chunk_z { + let ground_y = editor.get_ground_level((chunk_x << 4) + 8, (chunk_z << 4) + 8); + let mut rng = coord_rng(chunk_x, chunk_z, 0xC0DE); + + for ore in ORES { + let y_min = (ground_y - ore.depth_max).max(MIN_Y + 1); + let y_max = (ground_y - ore.depth_min).max(MIN_Y + 1); + if y_min > y_max { + continue; + } + let max_veins = ore.avg_veins_per_chunk * 2; + let n = rng.random_range(0..=max_veins); + for _ in 0..n { + let cx = (chunk_x << 4) + rng.random_range(0..16); + let cz = (chunk_z << 4) + rng.random_range(0..16); + let cy = rng.random_range(y_min..=y_max); + let size = rng.random_range(ore.vein_min..=ore.vein_max); + place_vein(editor, ore.block, cx, cy, cz, size, &mut rng); + } + } + } + } +} + +// Whitelist on set_block_absolute is required to overwrite STONE; pre-check filters AIR. +fn place_vein( + editor: &mut WorldEditor, + block: Block, + x: i32, + y: i32, + z: i32, + size: u32, + rng: &mut impl Rng, +) { + let (mut cx, mut cy, mut cz) = (x, y, z); + for _ in 0..size { + if editor.check_for_block_absolute(cx, cy, cz, Some(&[STONE]), None) { + editor.set_block_absolute(block, cx, cy, cz, Some(&[STONE]), None); + } + match rng.random_range(0..6) { + 0 => cx += 1, + 1 => cx -= 1, + 2 => cy += 1, + 3 => cy -= 1, + 4 => cz += 1, + _ => cz -= 1, + } + } +} diff --git a/src/world_editor/common.rs b/src/world_editor/common.rs index d88010a3b..3dd8ff9f1 100644 --- a/src/world_editor/common.rs +++ b/src/world_editor/common.rs @@ -7,6 +7,8 @@ use crate::block_definitions::*; /// Minimum Y coordinate in Minecraft (1.18+) pub const MIN_Y: i32 = -64; +/// Lowest section index covering MIN_Y (-64 / 16). +pub const MIN_SECTION_Y: i8 = (MIN_Y / 16) as i8; /// Maximum Y coordinate in Minecraft (data pack maximum: 2031) /// Vanilla limit is 319, but data packs can extend this up to 2031. /// The world editor supports the full range; the elevation system controls @@ -534,6 +536,40 @@ impl WorldToModify { } } + /// Fill empty (Uniform(AIR)) sections of a chunk up to `section_y_max` with + /// `Uniform(block)`. Returns true only if every section in the range was empty. + pub fn bulk_fill_chunk_sections_below( + &mut self, + chunk_x: i32, + chunk_z: i32, + section_y_max: i8, + block: Block, + ) -> bool { + if section_y_max < MIN_SECTION_Y { + return true; + } + let region_x = chunk_x >> 5; + let region_z = chunk_z >> 5; + let region = self.regions.entry((region_x, region_z)).or_default(); + let chunk = region + .chunks + .entry((chunk_x & 31, chunk_z & 31)) + .or_default(); + + let mut all_clean = true; + for section_y in MIN_SECTION_Y..=section_y_max { + let section = chunk.sections.entry(section_y).or_default(); + let is_empty = section.properties.is_empty() + && matches!(§ion.storage, BlockStorage::Uniform(b) if *b == AIR); + if is_empty { + section.storage = BlockStorage::Uniform(block); + } else { + all_clean = false; + } + } + all_clean + } + /// Scan every section and collapse any that are entirely one block type /// from `Full(Vec)` back to `Uniform(Block)`, freeing the 4 KiB allocation. pub fn compact_sections(&mut self) { @@ -548,3 +584,102 @@ impl WorldToModify { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bulk_fill_empty_chunk_all_clean() { + let mut world = WorldToModify::default(); + let all_clean = world.bulk_fill_chunk_sections_below(0, 0, -2, STONE); + assert!(all_clean, "fresh chunk should report all sections clean"); + + let region = world.get_region(0, 0).unwrap(); + let chunk = region.get_chunk(0, 0).unwrap(); + // Sections -4, -3, -2 must now exist as Uniform(STONE) + for y in MIN_SECTION_Y..=-2 { + let section = chunk + .sections + .get(&y) + .unwrap_or_else(|| panic!("section {y} should have been created")); + assert!( + matches!(§ion.storage, BlockStorage::Uniform(b) if *b == STONE), + "section {y} should be Uniform(STONE), got {:?}", + std::mem::discriminant(§ion.storage) + ); + assert!( + section.properties.is_empty(), + "section {y} should have no per-cell properties" + ); + } + } + + #[test] + fn bulk_fill_skips_occupied_section() { + let mut world = WorldToModify::default(); + // Pre-place a non-AIR block deep underground (section -2: y=-32..=-17) + // to simulate e.g. a bridge pier. + world.set_block_if_absent(0, -20, 0, COBBLESTONE); + + let all_clean = world.bulk_fill_chunk_sections_below(0, 0, -2, STONE); + assert!( + !all_clean, + "should return false because section -2 was occupied" + ); + + let region = world.get_region(0, 0).unwrap(); + let chunk = region.get_chunk(0, 0).unwrap(); + // Section -4 and -3 should be Uniform(STONE) + for y in [-4i8, -3] { + let section = chunk.sections.get(&y).unwrap(); + assert!( + matches!(§ion.storage, BlockStorage::Uniform(b) if *b == STONE), + "section {y} should be Uniform(STONE)" + ); + } + // Section -2 should be left alone (Full(Vec) with COBBLESTONE at y=-20) + let section = chunk.sections.get(&-2).unwrap(); + assert!( + matches!(§ion.storage, BlockStorage::Full(_)), + "section -2 should still be Full(Vec) (had COBBLESTONE)" + ); + // The pre-existing block must still be there + let local_y = (-20i32 & 15) as u8; + let idx = SectionToModify::index(0, local_y, 0); + assert_eq!( + section.storage.get(idx), + COBBLESTONE, + "pre-existing COBBLESTONE must not be overwritten" + ); + } + + #[test] + fn bulk_fill_below_min_section_is_noop() { + let mut world = WorldToModify::default(); + let all_clean = world.bulk_fill_chunk_sections_below(0, 0, MIN_SECTION_Y - 1, STONE); + assert!(all_clean, "below-min request should be vacuously clean"); + // No region should have been created + assert!(world.get_region(0, 0).is_none()); + } + + #[test] + fn bulk_fill_second_call_treats_existing_stone_as_occupied() { + // The "empty" check is strict Uniform(AIR). A second bulk-fill call + // on already-Uniform(STONE) sections sees them as occupied (returns + // false) but leaves them in their correct final state — calling + // bulk_fill twice is harmless. + let mut world = WorldToModify::default(); + assert!(world.bulk_fill_chunk_sections_below(0, 0, -2, STONE)); + let second = world.bulk_fill_chunk_sections_below(0, 0, -2, STONE); + assert!(!second, "second call sees Uniform(STONE) as occupied"); + let chunk = world.get_region(0, 0).unwrap().get_chunk(0, 0).unwrap(); + for y in MIN_SECTION_Y..=-2 { + let section = chunk.sections.get(&y).unwrap(); + assert!( + matches!(§ion.storage, BlockStorage::Uniform(b) if *b == STONE), + "section {y} should still be Uniform(STONE)" + ); + } + } +} diff --git a/src/world_editor/java.rs b/src/world_editor/java.rs index 707460d52..f33ec560f 100644 --- a/src/world_editor/java.rs +++ b/src/world_editor/java.rs @@ -75,6 +75,7 @@ impl<'a> WorldEditor<'a> { abs_chunk_x: i32, abs_chunk_z: i32, bake_lighting: bool, + biome_value: &Value, ) -> Result, Box> { // Use cached sections (computed once on first call) let sections = get_base_chunk_sections(); @@ -88,7 +89,7 @@ impl<'a> WorldEditor<'a> { other: FnvHashMap::default(), }; - let chunk_nbt = create_chunk_nbt(&chunk_data, bake_lighting); + let chunk_nbt = create_chunk_nbt(&chunk_data, bake_lighting, biome_value); let mut ser_buffer = Vec::with_capacity(8192); fastnbt::to_writer(&mut ser_buffer, &chunk_nbt)?; @@ -186,20 +187,33 @@ impl<'a> WorldEditor<'a> { let mut region = self.create_region(region_x, region_z)?; let mut ser_buffer = Vec::with_capacity(8192); + // World-center latitude drives temperature-based biome variants (taiga + // vs forest vs jungle) at chunk-build time. Cheap to recompute. + let center_lat = (self.llbbox.min().lat() + self.llbbox.max().lat()) * 0.5; + let ground_ref = self.ground.as_deref(); + // First pass: write all chunks that have content for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks { if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() { + let abs_chunk_x = chunk_x + (region_x * 32); + let abs_chunk_z = chunk_z + (region_z * 32); // Create chunk directly, we're writing to a fresh region file // so there's no existing data to preserve let chunk = Chunk { sections: chunk_to_modify.sections().collect(), - x_pos: chunk_x + (region_x * 32), - z_pos: chunk_z + (region_z * 32), + x_pos: abs_chunk_x, + z_pos: abs_chunk_z, is_light_on: 0, other: chunk_to_modify.other.clone(), }; - let chunk_nbt = create_chunk_nbt(&chunk, self.bake_lighting); + let biome_value = crate::biome::build_chunk_biome_nbt( + abs_chunk_x, + abs_chunk_z, + ground_ref, + center_lat, + ); + let chunk_nbt = create_chunk_nbt(&chunk, self.bake_lighting, &biome_value); ser_buffer.clear(); fastnbt::to_writer(&mut ser_buffer, &chunk_nbt)?; region.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)?; @@ -217,8 +231,18 @@ impl<'a> WorldEditor<'a> { // If chunk doesn't exist, create it with base layer if !chunk_exists { - let ser_buffer = - Self::create_base_chunk(abs_chunk_x, abs_chunk_z, self.bake_lighting)?; + let biome_value = crate::biome::build_chunk_biome_nbt( + abs_chunk_x, + abs_chunk_z, + ground_ref, + center_lat, + ); + let ser_buffer = Self::create_base_chunk( + abs_chunk_x, + abs_chunk_z, + self.bake_lighting, + &biome_value, + )?; region.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)?; } } @@ -612,7 +636,11 @@ fn compute_lighting( /// DataVersion, Status, yPos, Heightmaps, biomes, structures, etc. /// Section range is determined dynamically: at minimum the vanilla range /// (Y=-4 to Y=19), extended upward/downward to cover any sections with content. -fn create_chunk_nbt(chunk: &Chunk, bake_lighting: bool) -> HashMap { +fn create_chunk_nbt( + chunk: &Chunk, + bake_lighting: bool, + biome_value: &Value, +) -> HashMap { // Index existing sections by Y for quick lookup let section_map: HashMap = chunk .sections @@ -633,12 +661,6 @@ fn create_chunk_nbt(chunk: &Chunk, bake_lighting: bool) -> HashMap Value { + crate::biome::build_chunk_biome_nbt(0, 0, None, 0.0) + } + #[test] fn bake_lighting_writes_valid_light_arrays() { - let nbt = create_chunk_nbt(&grass_chunk(), true); + let nbt = create_chunk_nbt(&grass_chunk(), true, &plains_biome()); assert_eq!(nbt["isLightOn"], Value::Byte(1)); assert!(nbt.contains_key("Heightmaps")); let secs = sections(&nbt); @@ -1011,7 +1037,7 @@ mod tests { #[test] fn no_bake_lighting_omits_light_arrays() { - let nbt = create_chunk_nbt(&grass_chunk(), false); + let nbt = create_chunk_nbt(&grass_chunk(), false, &plains_biome()); assert_eq!(nbt["isLightOn"], Value::Byte(0)); for s in sections(&nbt) { let Value::Compound(m) = s else { panic!() }; @@ -1029,7 +1055,7 @@ mod tests { is_light_on: 0, other: FnvHashMap::default(), }; - let nbt = create_chunk_nbt(&chunk, true); + let nbt = create_chunk_nbt(&chunk, true, &plains_biome()); for s in sections(&nbt) { let Value::Compound(m) = s else { panic!() }; let Value::ByteArray(b) = &m["SkyLight"] else { diff --git a/src/world_editor/mod.rs b/src/world_editor/mod.rs index 49e60b5f9..2e420abad 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -16,7 +16,7 @@ mod luanti; pub mod bedrock; pub(crate) use common::WorldToModify; -pub use common::MIN_Y; +pub use common::{MIN_SECTION_Y, MIN_Y}; pub(crate) use bedrock::{BedrockSaveError, BedrockWriter}; @@ -1077,6 +1077,18 @@ impl<'a> WorldEditor<'a> { .fill_column(x, z, y_min, y_max, block, skip_existing); } + /// See [`WorldToModify::bulk_fill_chunk_sections_below`]. + pub fn bulk_fill_chunk_sections_below( + &mut self, + chunk_x: i32, + chunk_z: i32, + section_y_max: i8, + block: Block, + ) -> bool { + self.world + .bulk_fill_chunk_sections_below(chunk_x, chunk_z, section_y_max, block) + } + /// Saves all changes made to the world by writing to the appropriate format. /// /// Returns `Err` on I/O failure so callers can abort the generation pipeline