Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/bedrock_block_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
202 changes: 202 additions & 0 deletions src/biome.rs
Original file line number Diff line number Diff line change
@@ -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<i64> {
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");
}
}
20 changes: 14 additions & 6 deletions src/block_definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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);

Comment thread
louis-e marked this conversation as resolved.
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 {
Expand Down
4 changes: 4 additions & 0 deletions src/data_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 39 additions & 18 deletions src/element_processing/landuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,41 @@ 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<TreeType> = {
let mut trees: Vec<TreeType> = vec![];
if let Some(leaf_type) = element.tags.get("leaf_type") {
match leaf_type.as_str() {
"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
};
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down
Loading
Loading