From 28c8e54943228cc92dabaf3949ea38d44d76a0d3 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 2 May 2026 03:56:57 +0100 Subject: [PATCH 1/2] feat: generalize biome decoration rules --- modules/world-worldgen/src/biome_registry.zig | 70 +++++++++++-------- .../src/decoration_registry.zig | 17 ++++- .../world-worldgen/src/decoration_types.zig | 17 +++++ src/worldgen_tests.zig | 4 +- 4 files changed, 74 insertions(+), 34 deletions(-) diff --git a/modules/world-worldgen/src/biome_registry.zig b/modules/world-worldgen/src/biome_registry.zig index f07a5112..5921c358 100644 --- a/modules/world-worldgen/src/biome_registry.zig +++ b/modules/world-worldgen/src/biome_registry.zig @@ -4,6 +4,7 @@ const std = @import("std"); const BlockType = @import("world-core").BlockType; +const DecorationRule = @import("decoration_types.zig").DecorationRule; const tree_registry = @import("tree_registry.zig"); pub const TreeType = tree_registry.TreeType; @@ -43,16 +44,8 @@ pub const ColorTints = struct { /// Vegetation profile for biome-driven placement pub const VegetationProfile = struct { - tree_types: []const TreeType = &.{.oak}, - tree_density: f32 = 0.05, // Probability per attempt - bush_density: f32 = 0.0, - grass_density: f32 = 0.0, - cactus_density: f32 = 0.0, - dead_bush_density: f32 = 0.0, - bamboo_density: f32 = 0.0, - melon_density: f32 = 0.0, - red_mushroom_density: f32 = 0.0, - brown_mushroom_density: f32 = 0.0, + tree_types: []const TreeType = &.{}, + decoration_rules: []const DecorationRule = &.{}, }; /// Terrain modifiers applied during height computation @@ -233,7 +226,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = 0.0, .max = 0.20 }, .priority = 2, .surface = .{ .top = .gravel, .filler = .gravel, .depth_range = 4 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0 }, + .vegetation = .{ .tree_types = &.{} }, .colors = .{ .water = .{ 0.1, 0.2, 0.5 } }, }, .{ @@ -245,7 +238,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = 0.0, .max = 0.35 }, .priority = 1, .surface = .{ .top = .sand, .filler = .sand, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0 }, + .vegetation = .{ .tree_types = &.{} }, }, .{ .id = .beach, @@ -257,7 +250,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .max_slope = 2, .priority = 10, .surface = .{ .top = .sand, .filler = .sand, .depth_range = 2 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0 }, + .vegetation = .{ .tree_types = &.{} }, }, // === Land Biomes (continentalness > 0.45) === @@ -271,7 +264,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = Range.any(), .priority = 0, // Fallback .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{.sparse_oak}, .tree_density = 0.02, .grass_density = 0.3 }, + .vegetation = .{ .tree_types = &.{.sparse_oak}, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.3 }} }, .terrain = .{ .height_amplitude = 0.7, .smoothing = 0.2 }, }, .{ @@ -284,7 +277,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = .{ .min = 0.0, .max = 0.60 }, .priority = 5, .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{ .oak, .birch, .dense_oak }, .tree_density = 0.12, .bush_density = 0.05, .grass_density = 0.4 }, + .vegetation = .{ .tree_types = &.{ .oak, .birch, .dense_oak }, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.4 }} }, .colors = .{ .grass = .{ 0.18, 0.64, 0.16 }, .foliage = .{ 0.12, 0.52, 0.12 } }, }, .{ @@ -296,7 +289,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = 0.45, .max = 1.0 }, .priority = 6, .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{.spruce}, .tree_density = 0.10, .grass_density = 0.2 }, + .vegetation = .{ .tree_types = &.{.spruce}, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.2 }} }, .colors = .{ .grass = .{ 0.24, 0.56, 0.24 }, .foliage = .{ 0.18, 0.46, 0.18 } }, }, .{ @@ -311,7 +304,10 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .max_slope = 4, .priority = 6, .surface = .{ .top = .sand, .filler = .sand, .depth_range = 6 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0, .cactus_density = 0.015, .dead_bush_density = 0.02 }, + .vegetation = .{ .tree_types = &.{}, .decoration_rules = &.{ + .{ .block = .cactus, .place_on = &.{.sand}, .chance = 0.015 }, + .{ .block = .dead_bush, .place_on = &.{.sand}, .chance = 0.02 }, + } }, .terrain = .{ .height_amplitude = 0.5, .smoothing = 0.4 }, .colors = .{ .grass = .{ 0.75, 0.70, 0.35 } }, }, @@ -326,7 +322,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .max_slope = 3, .priority = 5, .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 2 }, - .vegetation = .{ .tree_types = &.{.swamp_oak}, .tree_density = 0.08 }, + .vegetation = .{ .tree_types = &.{.swamp_oak} }, .terrain = .{ .clamp_to_sea_level = true, .height_offset = -2 }, .colors = .{ .grass = .{ 0.24, 0.54, 0.20 }, @@ -345,7 +341,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .max_slope = 255, .priority = 4, .surface = .{ .top = .snow_block, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{.spruce}, .tree_density = 0.01 }, + .vegetation = .{ .tree_types = &.{.spruce} }, .colors = .{ .grass = .{ 0.7, 0.75, 0.8 } }, }, @@ -362,7 +358,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .min_ridge_mask = 0.1, .priority = 2, .surface = .{ .top = .stone, .filler = .stone, .depth_range = 1 }, - .vegetation = .{ .tree_types = &.{.sparse_oak}, .tree_density = 0 }, + .vegetation = .{ .tree_types = &.{.sparse_oak} }, .terrain = .{ .height_amplitude = 1.5 }, }, .{ @@ -377,7 +373,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .max_slope = 255, .priority = 2, .surface = .{ .top = .snow_block, .filler = .stone, .depth_range = 1 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0 }, + .vegetation = .{ .tree_types = &.{} }, .terrain = .{ .height_amplitude = 1.4 }, .colors = .{ .grass = .{ 0.85, 0.90, 0.95 } }, }, @@ -392,7 +388,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = 0.45, .max = 0.60 }, // Coastal swamp .priority = 6, .surface = .{ .top = .mud, .filler = .mud, .depth_range = 4 }, - .vegetation = .{ .tree_types = &.{.mangrove}, .tree_density = 0.15 }, + .vegetation = .{ .tree_types = &.{.mangrove} }, .terrain = .{ .clamp_to_sea_level = true, .height_offset = -1 }, .colors = .{ .grass = .{ 0.26, 0.58, 0.18 }, .foliage = .{ 0.22, 0.52, 0.16 }, .water = .{ 0.16, 0.38, 0.30 } }, }, @@ -405,7 +401,10 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = 0.60, .max = 1.0 }, // Inland .priority = 5, .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{.jungle}, .tree_density = 0.20, .bamboo_density = 0.08, .melon_density = 0.04 }, + .vegetation = .{ .tree_types = &.{.jungle}, .decoration_rules = &.{ + .{ .block = .bamboo, .place_on = &.{.grass}, .chance = 0.08 }, + .{ .block = .melon, .place_on = &.{.grass}, .chance = 0.04 }, + } }, .colors = .{ .grass = .{ 0.10, 0.76, 0.08 }, .foliage = .{ 0.08, 0.62, 0.08 } }, }, .{ @@ -417,7 +416,10 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = 0.55, .max = 1.0 }, // Inland (less restrictive) .priority = 5, // Higher priority to win over plains in hot zones .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{.acacia}, .tree_density = 0.015, .grass_density = 0.5, .dead_bush_density = 0.01 }, + .vegetation = .{ .tree_types = &.{.acacia}, .decoration_rules = &.{ + .{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.5 }, + .{ .block = .dead_bush, .place_on = &.{.grass}, .chance = 0.01 }, + } }, .colors = .{ .grass = .{ 0.55, 0.55, 0.30 }, .foliage = .{ 0.50, 0.50, 0.28 } }, }, .{ @@ -430,7 +432,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = .{ .min = 0.4, .max = 1.0 }, .priority = 6, .surface = .{ .top = .red_sand, .filler = .terracotta, .depth_range = 5 }, - .vegetation = .{ .cactus_density = 0.02 }, + .vegetation = .{ .tree_types = &.{}, .decoration_rules = &.{.{ .block = .cactus, .place_on = &.{.red_sand}, .chance = 0.02 }} }, .colors = .{ .grass = .{ 0.5, 0.4, 0.3 } }, }, .{ @@ -442,7 +444,10 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .max_height = 50, .priority = 20, .surface = .{ .top = .mycelium, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{ .huge_red_mushroom, .huge_brown_mushroom }, .tree_density = 0.05, .red_mushroom_density = 0.1, .brown_mushroom_density = 0.1 }, + .vegetation = .{ .tree_types = &.{ .huge_red_mushroom, .huge_brown_mushroom }, .decoration_rules = &.{ + .{ .block = .red_mushroom_block, .place_on = &.{.mycelium}, .chance = 0.1 }, + .{ .block = .brown_mushroom_block, .place_on = &.{.mycelium}, .chance = 0.1 }, + } }, .colors = .{ .grass = .{ 0.4, 0.8, 0.4 } }, }, .{ @@ -455,7 +460,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = -1.0, .max = -0.5 }, .priority = 15, .surface = .{ .top = .sand, .filler = .sand, .depth_range = 2 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0 }, + .vegetation = .{ .tree_types = &.{} }, }, // === Transition Micro-Biomes === @@ -472,7 +477,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = .{ .min = 0.30, .max = 0.80 }, .priority = 0, // Lowest priority .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{ .sparse_oak, .spruce }, .tree_density = 0.08, .grass_density = 0.4 }, + .vegetation = .{ .tree_types = &.{ .sparse_oak, .spruce }, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.4 }} }, .terrain = .{ .height_amplitude = 1.1, .smoothing = 0.1 }, .colors = .{ .grass = .{ 0.24, 0.62, 0.22 }, .foliage = .{ 0.18, 0.50, 0.16 } }, }, @@ -486,7 +491,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = .{ .min = 0.0, .max = 0.30 }, .priority = 0, // Lowest priority .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 2 }, - .vegetation = .{ .tree_types = &.{.swamp_oak}, .tree_density = 0.04, .grass_density = 0.5 }, + .vegetation = .{ .tree_types = &.{.swamp_oak}, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.5 }} }, .terrain = .{ .height_offset = -1, .smoothing = 0.3 }, .colors = .{ .grass = .{ 0.20, 0.58, 0.20 }, @@ -504,7 +509,10 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = .{ .min = 0.0, .max = 0.40 }, .priority = 0, // Lowest priority .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{.acacia}, .tree_density = 0.005, .grass_density = 0.3, .dead_bush_density = 0.02 }, + .vegetation = .{ .tree_types = &.{.acacia}, .decoration_rules = &.{ + .{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.3 }, + .{ .block = .dead_bush, .place_on = &.{.grass}, .chance = 0.02 }, + } }, .terrain = .{ .height_amplitude = 0.6, .smoothing = 0.25 }, .colors = .{ .grass = .{ 0.55, 0.50, 0.28 } }, // Less yellow, more natural }, @@ -518,7 +526,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .ruggedness = .{ .min = 0.0, .max = 0.35 }, .priority = 0, // Lowest priority .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{}, .tree_density = 0, .grass_density = 0.4 }, // No trees + .vegetation = .{ .tree_types = &.{}, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.4 }} }, // No trees .terrain = .{ .height_amplitude = 0.5, .smoothing = 0.3 }, .colors = .{ .grass = .{ 0.24, 0.66, 0.24 }, .foliage = .{ 0.18, 0.52, 0.16 } }, }, diff --git a/modules/world-worldgen/src/decoration_registry.zig b/modules/world-worldgen/src/decoration_registry.zig index 05f590cf..e21def4e 100644 --- a/modules/world-worldgen/src/decoration_registry.zig +++ b/modules/world-worldgen/src/decoration_registry.zig @@ -19,6 +19,7 @@ pub const SimpleDecoration = types.SimpleDecoration; pub const SchematicBlock = types.SchematicBlock; pub const Schematic = types.Schematic; pub const SchematicDecoration = types.SchematicDecoration; +pub const DecorationRule = types.DecorationRule; pub const Decoration = types.Decoration; pub const DecorationProvider = @import("decoration_provider.zig").DecorationProvider; pub const DecorationContext = @import("decoration_provider.zig").DecorationProvider.DecorationContext; @@ -172,10 +173,24 @@ pub const StandardDecorationProvider = struct { chunk.setBlock(local_x, @intCast(surface_y + 1), local_z, simple.block); } - // 2. Dynamic Tree Registry (from Biome Definition) + // 2. Biome-defined block decoration rules const biome_def = biome_mod.getBiomeDefinition(biome); const vegetation = biome_def.vegetation; + for (vegetation.decoration_rules) |rule| { + if (!rule.isAllowed(surface_block, surface_y, variant, allow_subbiomes)) continue; + + const prob = @min(1.0, rule.chance * veg_mult); + if (random.float(f32) >= prob) continue; + + const place_y = surface_y + 1; + if (chunk.getBlockSafe(@intCast(local_x), place_y, @intCast(local_z)) != .air) continue; + + chunk.setBlock(local_x, @intCast(place_y), local_z, rule.block); + break; + } + + // 3. Dynamic Tree Registry (from Biome Definition) if (vegetation.tree_types.len > 0) { for (vegetation.tree_types) |tree_type| { if (tree_registry.getTreeDefinition(tree_type)) |tree_def| { diff --git a/modules/world-worldgen/src/decoration_types.zig b/modules/world-worldgen/src/decoration_types.zig index 060e03c0..8bf31d95 100644 --- a/modules/world-worldgen/src/decoration_types.zig +++ b/modules/world-worldgen/src/decoration_types.zig @@ -59,6 +59,23 @@ pub const SchematicDecoration = struct { } }; +pub const DecorationRule = struct { + block: BlockType, + place_on: []const BlockType, + chance: f32, + y_min: i32 = 1, + y_max: i32 = 256, + variant_min: f32 = -1.0, + variant_max: f32 = 1.0, + + pub fn isAllowed(self: DecorationRule, surface_block: BlockType, surface_y: i32, variant: f32, allow_subbiomes: bool) bool { + if (!isBlockAllowed(self.place_on, surface_block)) return false; + if (surface_y + 1 < self.y_min or surface_y + 1 > self.y_max) return false; + if (allow_subbiomes) return variant >= self.variant_min and variant <= self.variant_max; + return self.variant_min == -1.0 and self.variant_max == 1.0; + } +}; + pub const Decoration = union(enum) { simple: SimpleDecoration, schematic: SchematicDecoration, diff --git a/src/worldgen_tests.zig b/src/worldgen_tests.zig index 2616b372..e31096bd 100644 --- a/src/worldgen_tests.zig +++ b/src/worldgen_tests.zig @@ -218,8 +218,8 @@ test "WorldGen stable chunk fingerprints for known seed" { }; const expected = [_]u64{ - 12740687495258888450, - 14029817033549788541, + 16067496184749289293, + 10106071382427144696, 4422434882344650145, }; From 02fdcaf044f5a4b95a34a436cac9c22186e28e9a Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sat, 2 May 2026 04:08:05 +0100 Subject: [PATCH 2/2] fix: guard decoration placement height --- modules/world-worldgen/src/decoration_registry.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/world-worldgen/src/decoration_registry.zig b/modules/world-worldgen/src/decoration_registry.zig index e21def4e..bf27c67a 100644 --- a/modules/world-worldgen/src/decoration_registry.zig +++ b/modules/world-worldgen/src/decoration_registry.zig @@ -108,6 +108,7 @@ fn variantAllowed(variant: f32, allow_subbiomes: bool, min: f32, max: f32) bool const Chunk = world_core.Chunk; const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; +const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; pub const StandardDecorationProvider = struct { @@ -170,7 +171,10 @@ pub const StandardDecorationProvider = struct { // 1. Static decorations (flowers, grass) if (chooseStaticSimpleDecoration(biome, surface_block, variant, allow_subbiomes, veg_mult, random)) |simple| { - chunk.setBlock(local_x, @intCast(surface_y + 1), local_z, simple.block); + const place_y = surface_y + 1; + if (place_y >= 0 and place_y < CHUNK_SIZE_Y) { + chunk.setBlock(local_x, @intCast(place_y), local_z, simple.block); + } } // 2. Biome-defined block decoration rules @@ -184,6 +188,7 @@ pub const StandardDecorationProvider = struct { if (random.float(f32) >= prob) continue; const place_y = surface_y + 1; + if (place_y < 0 or place_y >= CHUNK_SIZE_Y) continue; if (chunk.getBlockSafe(@intCast(local_x), place_y, @intCast(local_z)) != .air) continue; chunk.setBlock(local_x, @intCast(place_y), local_z, rule.block);