feat(generation): implement ancient city jigsaw structures (#2329)

This commit is contained in:
ZlordHUN
2026-07-01 09:11:47 +02:00
committed by GitHub
parent e47061b44b
commit 395c4aa36b
30 changed files with 1316 additions and 300 deletions

1
Cargo.lock generated
View File

@@ -2876,6 +2876,7 @@ dependencies = [
"rustc-hash",
"ruzstd",
"serde",
"serde_json",
"serde_json5",
"sha2 0.11.0",
"slotmap",

View File

@@ -81,6 +81,8 @@ pub struct StructureStruct {
pub step: String,
/// Optional jigsaw start pool.
pub start_pool: Option<String>,
/// Optional named jigsaw in the start pool that must be placed at the start position.
pub start_jigsaw_name: Option<String>,
/// Optional jigsaw size (depth).
pub size: Option<i32>,
/// Optional terrain adaptation (bearding).
@@ -222,6 +224,11 @@ impl ToTokens for StructureStruct {
} else {
quote!(None)
};
let start_jigsaw_name = if let Some(name) = &self.start_jigsaw_name {
quote!(Some(#name))
} else {
quote!(None)
};
let size = if let Some(s) = self.size {
quote!(Some(#s))
} else {
@@ -284,6 +291,7 @@ impl ToTokens for StructureStruct {
biomes: #biomes,
step: #step,
start_pool: #start_pool,
start_jigsaw_name: #start_jigsaw_name,
size: #size,
terrain_adaptation: #terrain_adaptation,
start_height: #start_height,
@@ -604,6 +612,7 @@ pub fn build() -> TokenStream {
pub biomes: &'static str,
pub step: GenerationStep,
pub start_pool: Option<&'static str>,
pub start_jigsaw_name: Option<&'static str>,
pub size: Option<i32>,
pub terrain_adaptation: TerrainAdaptation,
pub start_height: Option<i16>,

View File

@@ -131,6 +131,7 @@ pub struct Structure {
pub biomes: &'static str,
pub step: GenerationStep,
pub start_pool: Option<&'static str>,
pub start_jigsaw_name: Option<&'static str>,
pub size: Option<i32>,
pub terrain_adaptation: TerrainAdaptation,
pub start_height: Option<i16>,
@@ -175,6 +176,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ancient_city",
step: GenerationStep::UndergroundDecoration,
start_pool: Some("minecraft:ancient_city/city_center"),
start_jigsaw_name: Some("minecraft:city_anchor"),
size: Some(7i32),
terrain_adaptation: TerrainAdaptation::BeardBox,
start_height: Some(-27i16),
@@ -189,6 +191,7 @@ impl Structure {
biomes: "#minecraft:has_structure/bastion_remnant",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:bastion/starts"),
start_jigsaw_name: None,
size: Some(6i32),
terrain_adaptation: TerrainAdaptation::None,
start_height: Some(33i16),
@@ -203,6 +206,7 @@ impl Structure {
biomes: "#minecraft:has_structure/buried_treasure",
step: GenerationStep::UndergroundStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -217,6 +221,7 @@ impl Structure {
biomes: "#minecraft:has_structure/desert_pyramid",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -231,6 +236,7 @@ impl Structure {
biomes: "#minecraft:has_structure/end_city",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -245,6 +251,7 @@ impl Structure {
biomes: "#minecraft:has_structure/nether_fortress",
step: GenerationStep::UndergroundDecoration,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -259,6 +266,7 @@ impl Structure {
biomes: "#minecraft:has_structure/igloo",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -273,6 +281,7 @@ impl Structure {
biomes: "#minecraft:has_structure/jungle_temple",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -287,6 +296,7 @@ impl Structure {
biomes: "#minecraft:has_structure/woodland_mansion",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -301,6 +311,7 @@ impl Structure {
biomes: "#minecraft:has_structure/mineshaft",
step: GenerationStep::UndergroundStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -315,6 +326,7 @@ impl Structure {
biomes: "#minecraft:has_structure/mineshaft_mesa",
step: GenerationStep::UndergroundStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -329,6 +341,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ocean_monument",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -343,6 +356,7 @@ impl Structure {
biomes: "#minecraft:has_structure/nether_fossil",
step: GenerationStep::UndergroundDecoration,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: None,
@@ -357,6 +371,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ocean_ruin_cold",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -371,6 +386,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ocean_ruin_warm",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -385,6 +401,7 @@ impl Structure {
biomes: "#minecraft:has_structure/pillager_outpost",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:pillager_outpost/base_plates"),
start_jigsaw_name: None,
size: Some(7i32),
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: Some(0i16),
@@ -399,6 +416,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_standard",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -413,6 +431,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_desert",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -427,6 +446,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_jungle",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -441,6 +461,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_mountain",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -455,6 +476,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_nether",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -469,6 +491,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_ocean",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -483,6 +506,7 @@ impl Structure {
biomes: "#minecraft:has_structure/ruined_portal_swamp",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -497,6 +521,7 @@ impl Structure {
biomes: "#minecraft:has_structure/shipwreck",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -511,6 +536,7 @@ impl Structure {
biomes: "#minecraft:has_structure/shipwreck_beached",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -525,6 +551,7 @@ impl Structure {
biomes: "#minecraft:has_structure/stronghold",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::Bury,
start_height: None,
@@ -539,6 +566,7 @@ impl Structure {
biomes: "#minecraft:has_structure/swamp_hut",
step: GenerationStep::SurfaceStructures,
start_pool: None,
start_jigsaw_name: None,
size: None,
terrain_adaptation: TerrainAdaptation::None,
start_height: None,
@@ -553,6 +581,7 @@ impl Structure {
biomes: "#minecraft:has_structure/trail_ruins",
step: GenerationStep::UndergroundStructures,
start_pool: Some("minecraft:trail_ruins/tower"),
start_jigsaw_name: None,
size: Some(7i32),
terrain_adaptation: TerrainAdaptation::Bury,
start_height: Some(-15i16),
@@ -567,6 +596,7 @@ impl Structure {
biomes: "#minecraft:has_structure/trial_chambers",
step: GenerationStep::UndergroundStructures,
start_pool: Some("minecraft:trial_chambers/chamber/end"),
start_jigsaw_name: None,
size: Some(20i32),
terrain_adaptation: TerrainAdaptation::Encapsulate,
start_height: Some(-40i16),
@@ -581,6 +611,7 @@ impl Structure {
biomes: "#minecraft:has_structure/village_desert",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:village/desert/town_centers"),
start_jigsaw_name: None,
size: Some(6i32),
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: Some(0i16),
@@ -595,6 +626,7 @@ impl Structure {
biomes: "#minecraft:has_structure/village_plains",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:village/plains/town_centers"),
start_jigsaw_name: None,
size: Some(6i32),
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: Some(0i16),
@@ -609,6 +641,7 @@ impl Structure {
biomes: "#minecraft:has_structure/village_savanna",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:village/savanna/town_centers"),
start_jigsaw_name: None,
size: Some(6i32),
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: Some(0i16),
@@ -623,6 +656,7 @@ impl Structure {
biomes: "#minecraft:has_structure/village_snowy",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:village/snowy/town_centers"),
start_jigsaw_name: None,
size: Some(6i32),
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: Some(0i16),
@@ -637,6 +671,7 @@ impl Structure {
biomes: "#minecraft:has_structure/village_taiga",
step: GenerationStep::SurfaceStructures,
start_pool: Some("minecraft:village/taiga/town_centers"),
start_jigsaw_name: None,
size: Some(6i32),
terrain_adaptation: TerrainAdaptation::BeardThin,
start_height: Some(0i16),

View File

@@ -29,6 +29,7 @@ tokio = { workspace = true, features = [
uuid.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
crossbeam.workspace = true

View File

@@ -0,0 +1 @@
{"processors":[{"integrity":0.95,"processor_type":"minecraft:block_rot","rottable_blocks":"#minecraft:ancient_city_replaceable"},{"processor_type":"minecraft:rule","rules":[{"input_predicate":{"block":"minecraft:deepslate_bricks","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:cracked_deepslate_bricks"}},{"input_predicate":{"block":"minecraft:deepslate_tiles","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:cracked_deepslate_tiles"}},{"input_predicate":{"block":"minecraft:soul_lantern","predicate_type":"minecraft:random_block_match","probability":0.05},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:air"}}]},{"processor_type":"minecraft:protected_blocks","value":"#minecraft:features_cannot_replace"}]}

View File

@@ -0,0 +1 @@
{"processors":[{"processor_type":"minecraft:rule","rules":[{"input_predicate":{"block":"minecraft:deepslate_bricks","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:cracked_deepslate_bricks"}},{"input_predicate":{"block":"minecraft:deepslate_tiles","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:cracked_deepslate_tiles"}},{"input_predicate":{"block":"minecraft:soul_lantern","predicate_type":"minecraft:random_block_match","probability":0.05},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:air"}}]},{"processor_type":"minecraft:protected_blocks","value":"#minecraft:features_cannot_replace"}]}

View File

@@ -0,0 +1 @@
{"processors":[{"integrity":0.95,"processor_type":"minecraft:block_rot","rottable_blocks":"#minecraft:ancient_city_replaceable"},{"processor_type":"minecraft:rule","rules":[{"input_predicate":{"block":"minecraft:deepslate_bricks","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:cracked_deepslate_bricks"}},{"input_predicate":{"block":"minecraft:deepslate_tiles","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:cracked_deepslate_tiles"}},{"input_predicate":{"block":"minecraft:deepslate_tile_slab","predicate_type":"minecraft:random_block_match","probability":0.3},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:air"}},{"input_predicate":{"block":"minecraft:soul_lantern","predicate_type":"minecraft:random_block_match","probability":0.05},"location_predicate":{"predicate_type":"minecraft:always_true"},"output_state":{"Name":"minecraft:air"}}]},{"processor_type":"minecraft:protected_blocks","value":"#minecraft:features_cannot_replace"}]}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city/entrance/entrance_connector","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city/entrance/entrance_path_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city/entrance/entrance_path_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city/entrance/entrance_path_3","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city/entrance/entrance_path_4","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city/entrance/entrance_path_5","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1}],"fallback":"minecraft:empty"}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/city_center_1","processors":"minecraft:ancient_city_start_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/city_center_2","processors":"minecraft:ancient_city_start_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/city_center_3","processors":"minecraft:ancient_city_start_degradation","projection":"rigid"},"weight":1}],"fallback":"minecraft:empty"}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/bottom_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/bottom_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/bottom_left_corner","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/bottom_right_corner_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/bottom_right_corner_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/left","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/right","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/top","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/top_right_corner","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/city_center/walls/top_left_corner","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1}],"fallback":"minecraft:empty"}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:feature_pool_element","feature":"minecraft:sculk_patch_ancient_city","projection":"rigid"},"weight":6},{"element":{"element_type":"minecraft:empty_pool_element"},"weight":1}],"fallback":"minecraft:empty"}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:empty_pool_element"},"weight":7},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/barracks","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/chamber_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/chamber_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/chamber_3","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/sauna_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/small_statue","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/large_ruin_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/tall_ruin_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/tall_ruin_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/tall_ruin_3","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":2},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/tall_ruin_4","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":2},{"element":{"element_type":"minecraft:list_pool_element","elements":[{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/camp_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/camp_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/camp_3","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"}],"projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/medium_ruin_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/medium_ruin_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/small_ruin_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/small_ruin_2","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/large_pillar_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/medium_pillar_1","processors":"minecraft:ancient_city_generic_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:list_pool_element","elements":[{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/structures/ice_box_1","processors":{"processors":[]},"projection":"rigid"}],"projection":"rigid"},"weight":1}],"fallback":"minecraft:empty"}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_corner_wall_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_intersection_wall_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_lshape_wall_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_2","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_2","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_3","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_4","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":4},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_passage_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":3},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/ruined_corner_wall_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/ruined_corner_wall_2","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/ruined_horizontal_wall_stairs_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":2},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/ruined_horizontal_wall_stairs_2","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":2},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/ruined_horizontal_wall_stairs_3","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":3},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/ruined_horizontal_wall_stairs_4","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":3}],"fallback":"minecraft:empty"}

View File

@@ -0,0 +1 @@
{"elements":[{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_2","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_1","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_2","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_3","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_4","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_stairs_5","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1},{"element":{"element_type":"minecraft:single_pool_element","location":"minecraft:ancient_city/walls/intact_horizontal_wall_bridge","processors":"minecraft:ancient_city_walls_degradation","projection":"rigid"},"weight":1}],"fallback":"minecraft:empty"}

View File

@@ -12,6 +12,12 @@ fn main() {
let mut pool_code = String::from(
"pub fn get_pool_elements(pool_id: &str) -> Option<&'static [&'static str]> {\n match pool_id {\n",
);
let mut template_pool_json_code = String::from(
"pub fn get_template_pool_json(path: &str) -> Option<&'static str> {\n match path {\n",
);
let mut processor_list_json_code = String::from(
"pub fn get_processor_list_json(path: &str) -> Option<&'static str> {\n match path {\n",
);
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let assets_dir = Path::new(&manifest_dir).join("assets/structures");
@@ -36,8 +42,29 @@ fn main() {
pool_code.push_str(" _ => None,\n");
pool_code.push_str(" }\n}\n");
fs::write(&dest_path, format!("{code}\n{pool_code}")).unwrap();
let worldgen_dir = Path::new(&manifest_dir).join("assets/worldgen");
process_json_dir(
&worldgen_dir.join("template_pool"),
"",
&mut template_pool_json_code,
);
process_json_dir(
&worldgen_dir.join("processor_list"),
"",
&mut processor_list_json_code,
);
template_pool_json_code.push_str(" _ => None,\n");
template_pool_json_code.push_str(" }\n}\n");
processor_list_json_code.push_str(" _ => None,\n");
processor_list_json_code.push_str(" }\n}\n");
fs::write(
&dest_path,
format!("{code}\n{pool_code}\n{template_pool_json_code}\n{processor_list_json_code}"),
)
.unwrap();
println!("cargo:rerun-if-changed=assets/structures");
println!("cargo:rerun-if-changed=assets/worldgen");
}
fn process_dir(
@@ -76,3 +103,39 @@ fn process_dir(
}
}
}
fn process_json_dir(dir: &Path, prefix: &str, code: &mut String) {
if !dir.exists() {
return;
}
let mut entries = fs::read_dir(dir)
.unwrap()
.map(Result::unwrap)
.collect::<Vec<_>>();
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let name = entry.file_name().into_string().unwrap();
if path.is_dir() {
let new_prefix = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
process_json_dir(&path, &new_prefix, code);
} else if let Some(stem) = name.strip_suffix(".json") {
let id = if prefix.is_empty() {
stem.to_string()
} else {
format!("{prefix}/{stem}")
};
let abs_path = path.canonicalize().unwrap();
code.push_str(&format!(
" \"minecraft:{id}\" | \"{id}\" => Some(include_str!(r#\"{}\"#)),\n",
abs_path.display()
));
}
}
}

View File

@@ -129,4 +129,57 @@ mod tests {
StagedChunkEnum::Full,
);
}
#[test]
fn configured_seed_generates_vanilla_ancient_city_chunk() {
let dimension = Dimension::OVERWORLD;
let seed = Seed(1_782_124_772_053_846_960);
let block_registry = Arc::new(BlockRegistry);
let world_gen = get_world_gen(seed, dimension.clone());
let biome_mixer_seed = hash_seed(world_gen.random_config.seed);
let chunk = generate_single_chunk(
&dimension,
biome_mixer_seed,
&world_gen,
block_registry.as_ref(),
31,
-12,
StagedChunkEnum::Features,
);
let super::Chunk::Proto(chunk) = chunk else {
panic!("features stage should return a proto chunk");
};
let mut city_blocks = 0;
let mut jigsaw_blocks = 0;
for x in 496..512 {
for z in -192..-176 {
for y in -64..320 {
let block = chunk
.get_block_state(&pumpkin_util::math::vector3::Vector3::new(x, y, z))
.to_block_id();
if [
pumpkin_data::Block::DEEPSLATE_BRICKS.id,
pumpkin_data::Block::POLISHED_DEEPSLATE.id,
pumpkin_data::Block::REINFORCED_DEEPSLATE.id,
pumpkin_data::Block::SCULK.id,
]
.contains(&block)
{
city_blocks += 1;
}
if block == pumpkin_data::Block::JIGSAW.id {
jigsaw_blocks += 1;
}
}
}
}
assert!(
city_blocks > 0,
"reference chunk contains no Ancient City blocks"
);
assert_eq!(jigsaw_blocks, 0, "jigsaw blocks were not replaced");
}
}

View File

@@ -1063,6 +1063,31 @@ impl GenerationSchedule {
}
node.in_flight = true;
let node = node.clone();
// A chunk can be advanced as part of a neighboring task's write cache.
// In that case its queued node may survive even though the returned
// ProtoChunk has already reached this stage. Dispatching the stale node
// would run the same stage twice and trip ProtoChunk's stage invariant.
let actual_stage = self
.chunk_map
.get(&node.pos)
.and_then(|holder| holder.chunk.as_ref())
.map(Chunk::get_stage_id);
if actual_stage.is_some_and(|stage| stage >= node.stage as u8) {
if let Some(holder) = self.chunk_map.get_mut(&node.pos) {
holder.current_stage = holder
.current_stage
.max(StagedChunkEnum::from(actual_stage.expect("checked above")));
let task_slot = &mut holder.tasks[node.stage as usize];
if *task_slot == task.1 {
*task_slot = NodeKey::null();
}
}
self.waiting_for_chunks.remove(&task.1);
self.drop_node(task.1);
continue;
}
if node.stage == StagedChunkEnum::Empty {
self.running_task_count += 1;
let holder = self.chunk_map.get_mut(&node.pos).unwrap();

View File

@@ -1,5 +1,8 @@
use pumpkin_data::Block;
use pumpkin_data::tag::Block::MINECRAFT_SCULK_REPLACEABLE_WORLD_GEN;
use pumpkin_data::{
Block, BlockState,
block_properties::{BlockProperties, GlowLichenLikeProperties, SculkShriekerLikeProperties},
};
use pumpkin_util::{
math::{int_provider::IntProvider, position::BlockPos, vector3::Vector3},
random::{RandomGenerator, RandomImpl},
@@ -69,8 +72,57 @@ impl SculkPatchFeature {
GenerationCache::get_block_state(chunk, &below_candidate.0).to_state();
if state.is_air() && below_state.is_side_solid(pumpkin_data::BlockDirection::Up) {
// TODO: set sculk shrieker with can_summon = true if possible
chunk.set_block_state(&candidate.0, Block::SCULK_SHRIEKER.default_state);
chunk.set_block_state(&candidate.0, ancient_city_shrieker_state());
}
}
true
}
pub fn generate_in_proto_chunk(
&self,
chunk: &mut crate::ProtoChunk,
random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
if !can_spread_from_proto_chunk(chunk, pos) {
return false;
}
let mut spreader = SculkSpreader::new();
let total_rounds = self.spread_rounds + self.growth_rounds;
for round in 0..total_rounds {
for _ in 0..self.charge_count {
spreader.add_cursor(pos, self.amount_per_charge);
}
for _ in 0..self.spread_attempts {
spreader.update_cursors_in_proto_chunk(chunk, random, round < self.spread_rounds);
}
spreader.clear();
}
let below = pos.down();
if random.next_f32() <= self.catalyst_chance
&& proto_chunk_state(chunk, below).is_some_and(|state| state.to_state().is_solid())
{
set_proto_chunk_state(chunk, pos, Block::SCULK_CATALYST.default_state);
}
for _ in 0..self.extra_rare_growths.get(random) {
let candidate = pos.offset(Vector3::new(
random.next_bounded_i32(5) - 2,
0,
random.next_bounded_i32(5) - 2,
));
let below = candidate.down();
if proto_chunk_state(chunk, candidate).is_some_and(|state| state.to_state().is_air())
&& proto_chunk_state(chunk, below).is_some_and(|state| {
state
.to_state()
.is_side_solid(pumpkin_data::BlockDirection::Up)
})
{
set_proto_chunk_state(chunk, candidate, ancient_city_shrieker_state());
}
}
@@ -116,10 +168,52 @@ fn is_sculk_behaviour(block_id: u16) -> bool {
|| block_id == Block::CALIBRATED_SCULK_SENSOR.id
}
fn ancient_city_shrieker_state() -> &'static BlockState {
let mut properties = SculkShriekerLikeProperties::default(&Block::SCULK_SHRIEKER);
properties.r#can_summon = true;
BlockState::from_id(properties.to_state_id(&Block::SCULK_SHRIEKER))
}
fn is_sculk_replaceable(block_id: u16) -> bool {
MINECRAFT_SCULK_REPLACEABLE_WORLD_GEN.1.contains(&block_id)
}
/// Resolves the sculk-vein block state to place at a position so that it clings to a sturdy
/// neighbour on `face` (the direction from the vein position toward that neighbour). Existing
/// sculk-vein states are merged; replaceable/air/water blocks become a fresh vein.
fn sculk_vein_state_with_face(
existing: &'static BlockState,
face: pumpkin_data::BlockDirection,
) -> Option<&'static BlockState> {
let existing_block_id = Block::get_raw_id_from_state_id(existing.id);
let is_vein = existing_block_id == Block::SCULK_VEIN.id;
if !(is_vein || is_sculk_replaceable(existing_block_id) || existing_block_id == Block::WATER.id)
{
return None;
}
let mut properties = if is_vein {
GlowLichenLikeProperties::from_state_id(existing.id, &Block::SCULK_VEIN)
} else {
let mut properties = GlowLichenLikeProperties::default(&Block::SCULK_VEIN);
properties.r#waterlogged = existing_block_id == Block::WATER.id;
properties
};
match face {
pumpkin_data::BlockDirection::Down => properties.r#down = true,
pumpkin_data::BlockDirection::Up => properties.r#up = true,
pumpkin_data::BlockDirection::North => properties.r#north = true,
pumpkin_data::BlockDirection::South => properties.r#south = true,
pumpkin_data::BlockDirection::West => properties.r#west = true,
pumpkin_data::BlockDirection::East => properties.r#east = true,
}
Some(BlockState::from_id(
properties.to_state_id(&Block::SCULK_VEIN),
))
}
struct Cursor {
pos: BlockPos,
charge: i32,
@@ -148,7 +242,7 @@ impl SculkSpreader {
&mut self,
chunk: &mut T,
random: &mut RandomGenerator,
_spread_veins: bool,
spread_veins: bool,
) {
let mut next_cursors = Vec::new();
for mut cursor in self.cursors.drain(..) {
@@ -167,6 +261,49 @@ impl SculkSpreader {
if is_sculk_replaceable(target_block_id) {
chunk.set_block_state(&target_pos.0, Block::SCULK.default_state);
if spread_veins {
grow_sculk_veins(chunk, target_pos);
}
cursor.pos = target_pos;
cursor.charge -= 1;
} else if target_block_id == Block::SCULK.id {
cursor.pos = target_pos;
cursor.charge -= 1;
}
if cursor.charge > 0 {
next_cursors.push(cursor);
}
}
self.cursors = next_cursors;
}
fn update_cursors_in_proto_chunk(
&mut self,
chunk: &mut crate::ProtoChunk,
random: &mut RandomGenerator,
spread_veins: bool,
) {
let mut next_cursors = Vec::new();
for mut cursor in self.cursors.drain(..) {
if cursor.charge <= 0 {
continue;
}
let target_pos = cursor.pos.offset(Vector3::new(
random.next_bounded_i32(3) - 1,
random.next_bounded_i32(3) - 1,
random.next_bounded_i32(3) - 1,
));
let Some(target_state) = proto_chunk_state(chunk, target_pos) else {
continue;
};
let target_block_id = target_state.to_block_id();
if is_sculk_replaceable(target_block_id) {
set_proto_chunk_state(chunk, target_pos, Block::SCULK.default_state);
if spread_veins {
grow_sculk_veins_in_proto_chunk(chunk, target_pos);
}
cursor.pos = target_pos;
cursor.charge -= 1;
} else if target_block_id == Block::SCULK.id {
@@ -181,3 +318,69 @@ impl SculkSpreader {
self.cursors = next_cursors;
}
}
fn proto_chunk_state(
chunk: &crate::ProtoChunk,
pos: BlockPos,
) -> Option<crate::block::RawBlockState> {
((pos.0.x >> 4) == chunk.x && (pos.0.z >> 4) == chunk.z).then(|| chunk.get_block_state(&pos.0))
}
fn set_proto_chunk_state(
chunk: &mut crate::ProtoChunk,
pos: BlockPos,
state: &'static pumpkin_data::BlockState,
) {
if (pos.0.x >> 4) == chunk.x && (pos.0.z >> 4) == chunk.z {
chunk.set_block_state(pos.0.x, pos.0.y, pos.0.z, state);
}
}
fn grow_sculk_veins<T: GenerationCache>(chunk: &mut T, sculk_pos: BlockPos) {
for dir in pumpkin_data::BlockDirection::all() {
let vein_pos = sculk_pos.offset(dir.to_offset());
let existing = GenerationCache::get_block_state(chunk, &vein_pos.0).to_state();
if let Some(state) = sculk_vein_state_with_face(existing, dir.opposite()) {
chunk.set_block_state(&vein_pos.0, state);
}
}
}
fn grow_sculk_veins_in_proto_chunk(chunk: &mut crate::ProtoChunk, sculk_pos: BlockPos) {
for dir in pumpkin_data::BlockDirection::all() {
let vein_pos = sculk_pos.offset(dir.to_offset());
let Some(existing) = proto_chunk_state(chunk, vein_pos) else {
continue;
};
if let Some(state) = sculk_vein_state_with_face(existing.to_state(), dir.opposite()) {
set_proto_chunk_state(chunk, vein_pos, state);
}
}
}
fn can_spread_from_proto_chunk(chunk: &crate::ProtoChunk, pos: BlockPos) -> bool {
let Some(state) = proto_chunk_state(chunk, pos) else {
return false;
};
let block_id = state.to_block_id();
if is_sculk_behaviour(block_id) {
return true;
}
if !state.to_state().is_air() && block_id != Block::WATER.id {
return false;
}
[
Vector3::new(1, 0, 0),
Vector3::new(-1, 0, 0),
Vector3::new(0, 1, 0),
Vector3::new(0, -1, 0),
Vector3::new(0, 0, 1),
Vector3::new(0, 0, -1),
]
.into_iter()
.any(|offset| {
proto_chunk_state(chunk, pos.offset(offset))
.is_some_and(|state| state.to_state().is_solid())
})
}

View File

@@ -49,6 +49,30 @@ pub enum Feature {
}
impl PlacedFeature {
pub fn generate_in_proto_chunk(
&self,
chunk: &mut crate::ProtoChunk,
feature_name: pumpkin_data::placed_feature::PlacedFeature,
random: &mut RandomGenerator,
pos: BlockPos,
) -> bool {
let feature = match &self.feature {
Feature::Named(name) => CONFIGURED_FEATURES
.get(name)
.expect("Name: {name:?} not found"),
Feature::Inlined(feature) => feature,
};
match feature {
ConfiguredFeature::SculkPatch(feature) => {
feature.generate_in_proto_chunk(chunk, random, pos)
}
_ => {
tracing::warn!("Placed feature {feature_name:?} is not supported in a jigsaw pool");
false
}
}
}
#[expect(clippy::too_many_arguments)]
pub fn generate<T: GenerationCache>(
&self,

View File

@@ -83,12 +83,15 @@ pub fn try_generate_structure(
| StructureKeys::PillagerOutpost
| StructureKeys::TrailRuins
| StructureKeys::TrialChambers => {
let generator = JigsawGenerator::new(
let mut generator = JigsawGenerator::new(
structure
.start_pool
.expect("Jigsaw structure must have a start pool"),
structure.size.expect("Jigsaw structure must have a size"),
);
if let Some(start_jigsaw_name) = structure.start_jigsaw_name {
generator = generator.with_start_jigsaw(start_jigsaw_name);
}
generator.get_structure_position(context)
}
// TODO: Implement other structure types
@@ -166,12 +169,15 @@ pub fn lazily_generate_structure(
| StructureKeys::PillagerOutpost
| StructureKeys::TrailRuins
| StructureKeys::TrialChambers => {
let generator = JigsawGenerator::new(
let mut generator = JigsawGenerator::new(
structure
.start_pool
.expect("Jigsaw structure must have a start pool"),
structure.size.expect("Jigsaw structure must have a size"),
);
if let Some(start_jigsaw_name) = structure.start_jigsaw_name {
generator = generator.with_start_jigsaw(start_jigsaw_name);
}
generator.get_structure_position(context)
}
// TODO: Implement other structure types

View File

@@ -178,6 +178,7 @@ impl StructurePieceBase for IglooPiece {
(0, 0),
self.rotation,
false,
false,
&[],
Some(chunk_box),
);
@@ -209,6 +210,7 @@ impl StructurePieceBase for IglooPiece {
(SHAFT_OFFSET_X, SHAFT_OFFSET_Z),
self.rotation,
false,
false,
&[],
Some(chunk_box),
);
@@ -227,6 +229,7 @@ impl StructurePieceBase for IglooPiece {
(BASEMENT_OFFSET_X, BASEMENT_OFFSET_Z),
self.rotation,
false,
false,
&[],
Some(chunk_box),
);

View File

@@ -5,10 +5,11 @@ use crate::generation::structure::structures::{
StructureGenerator, StructureGeneratorContext, StructurePieceBase, StructurePosition,
};
use crate::generation::structure::template::{
BlockMirror, BlockRotation, BlockStateResolver, PaletteEntry, StructureTemplate,
BlockMirror, BlockRotation, PaletteEntry, StructureTemplate,
};
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::random::RandomImpl;
use serde::Deserialize;
use std::sync::Arc;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -26,9 +27,193 @@ pub struct TemplatePool {
#[derive(Clone)]
pub struct PoolElement {
pub template: &'static str,
pub weight: u32,
pub projection: JigsawProjection,
pub kind: PoolElementKind,
}
#[derive(Clone)]
pub enum PoolElementKind {
Empty,
Single {
template: String,
processors: ProcessorListRef,
},
List(Vec<PoolElementKind>),
Feature(pumpkin_data::placed_feature::PlacedFeature),
}
#[derive(Clone, Default)]
pub enum ProcessorListRef {
Named(String),
#[default]
Empty,
}
#[derive(Deserialize)]
struct RawTemplatePool {
fallback: String,
elements: Vec<RawWeightedPoolElement>,
}
#[derive(Deserialize)]
struct RawWeightedPoolElement {
element: RawPoolElement,
weight: u32,
}
#[derive(Deserialize)]
#[serde(tag = "element_type")]
enum RawPoolElement {
#[serde(rename = "minecraft:empty_pool_element")]
Empty,
#[serde(rename = "minecraft:single_pool_element")]
Single {
location: String,
processors: RawProcessorList,
projection: RawProjection,
},
#[serde(rename = "minecraft:list_pool_element")]
List {
elements: Vec<RawPoolElement>,
projection: RawProjection,
},
#[serde(rename = "minecraft:feature_pool_element")]
Feature {
feature: String,
projection: RawProjection,
},
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RawProcessorList {
Named(String),
Inline { processors: Vec<serde_json::Value> },
}
#[derive(Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
enum RawProjection {
Rigid,
TerrainMatching,
}
impl From<RawProjection> for JigsawProjection {
fn from(value: RawProjection) -> Self {
match value {
RawProjection::Rigid => Self::Rigid,
RawProjection::TerrainMatching => Self::TerrainMatching,
}
}
}
impl RawPoolElement {
fn into_element(self) -> Option<(PoolElementKind, JigsawProjection)> {
match self {
Self::Empty => Some((PoolElementKind::Empty, JigsawProjection::Rigid)),
Self::Single {
location,
processors,
projection,
} => {
let processors = match processors {
RawProcessorList::Named(name) => ProcessorListRef::Named(name),
RawProcessorList::Inline { processors } => {
debug_assert!(processors.is_empty());
ProcessorListRef::Empty
}
};
Some((
PoolElementKind::Single {
template: location,
processors,
},
projection.into(),
))
}
Self::List {
elements,
projection,
} => {
let projection = projection.into();
let elements = elements
.into_iter()
.filter_map(|element| element.into_element().map(|(kind, _)| kind))
.collect();
Some((PoolElementKind::List(elements), projection))
}
Self::Feature {
feature,
projection,
} => {
let feature = feature.strip_prefix("minecraft:").unwrap_or(&feature);
pumpkin_data::placed_feature::PlacedFeature::from_name(feature)
.map(|feature| (PoolElementKind::Feature(feature), projection.into()))
}
}
}
}
impl PoolElement {
#[must_use]
pub const fn is_empty(&self) -> bool {
matches!(self.kind, PoolElementKind::Empty)
}
#[must_use]
pub fn first_template(&self) -> Option<Arc<StructureTemplate>> {
fn find(kind: &PoolElementKind) -> Option<Arc<StructureTemplate>> {
match kind {
PoolElementKind::Single { template, .. } => {
crate::generation::structure::template::get_template(template)
}
PoolElementKind::List(elements) => elements.iter().find_map(find),
PoolElementKind::Empty | PoolElementKind::Feature(_) => None,
}
}
find(&self.kind)
}
pub fn for_each_template(
&self,
mut consumer: impl FnMut(&str, &ProcessorListRef, Arc<StructureTemplate>),
) {
fn visit(
kind: &PoolElementKind,
consumer: &mut impl FnMut(&str, &ProcessorListRef, Arc<StructureTemplate>),
) {
match kind {
PoolElementKind::Single {
template,
processors,
} => {
if let Some(structure_template) =
crate::generation::structure::template::get_template(template)
{
consumer(template, processors, structure_template);
}
}
PoolElementKind::List(elements) => {
for element in elements {
visit(element, consumer);
}
}
PoolElementKind::Empty | PoolElementKind::Feature(_) => {}
}
}
visit(&self.kind, &mut consumer);
}
#[must_use]
pub fn feature(&self) -> Option<pumpkin_data::placed_feature::PlacedFeature> {
match self.kind {
PoolElementKind::Feature(feature) => Some(feature),
_ => None,
}
}
}
impl TemplatePool {
@@ -53,27 +238,91 @@ impl TemplatePool {
/// Discovers a pool from the filesystem/embedded assets.
#[must_use]
pub fn discover(id: &str) -> Option<Self> {
let elements = crate::generation::structure::template::get_pool_elements(id)?;
static CACHE: std::sync::LazyLock<dashmap::DashMap<String, TemplatePool>> =
std::sync::LazyLock::new(dashmap::DashMap::new);
// Heuristic: roads and streets are usually TerrainMatching
let projection = if id.contains("streets") {
JigsawProjection::TerrainMatching
} else {
JigsawProjection::Rigid
};
if let Some(pool) = CACHE.get(id) {
return Some(pool.clone());
}
Some(Self {
id: id.to_string(),
fallback: "minecraft:empty".to_string(),
elements: elements
.iter()
.map(|e| PoolElement {
template: e,
weight: 1,
projection,
let pool = if id == "minecraft:empty" || id == "empty" {
Self {
id: "minecraft:empty".to_string(),
fallback: "minecraft:empty".to_string(),
elements: Vec::new(),
}
} else if let Some(json) =
crate::generation::structure::template::get_template_pool_json(id)
{
let raw: RawTemplatePool = match serde_json::from_str(json) {
Ok(pool) => pool,
Err(error) => {
tracing::error!("Failed to parse template pool {id}: {error}");
return None;
}
};
let elements = raw
.elements
.into_iter()
.filter_map(|weighted| {
weighted
.element
.into_element()
.map(|(kind, projection)| PoolElement {
weight: weighted.weight,
projection,
kind,
})
})
.collect(),
})
.collect();
Self {
id: id.to_string(),
fallback: raw.fallback,
elements,
}
} else {
let elements = crate::generation::structure::template::get_pool_elements(id)?;
let projection = if id.contains("streets") {
JigsawProjection::TerrainMatching
} else {
JigsawProjection::Rigid
};
Self {
id: id.to_string(),
fallback: "minecraft:empty".to_string(),
elements: elements
.iter()
.map(|e| PoolElement {
weight: 1,
projection,
kind: PoolElementKind::Single {
template: (*e).to_string(),
processors: ProcessorListRef::Empty,
},
})
.collect(),
}
};
CACHE.insert(id.to_owned(), pool.clone());
Some(pool)
}
#[must_use]
pub fn get_shuffled_elements(
&self,
random: &mut pumpkin_util::random::RandomGenerator,
) -> Vec<PoolElement> {
let mut elements = self
.elements
.iter()
.flat_map(|element| std::iter::repeat_n(element.clone(), element.weight as usize))
.collect::<Vec<_>>();
for index in (1..elements.len()).rev() {
let other = random.next_bounded_i32(index as i32 + 1) as usize;
elements.swap(index, other);
}
elements
}
}
@@ -189,7 +438,7 @@ pub struct JigsawJunction {
pub struct PoolElementStructurePiece {
pub piece: crate::generation::structure::structures::StructurePiece,
pub template: Arc<StructureTemplate>,
pub element: PoolElement,
pub pos: BlockPos,
pub rotation: BlockRotation,
pub mirror: BlockMirror,
@@ -218,55 +467,39 @@ impl StructurePieceBase for PoolElementStructurePiece {
&mut self,
chunk: &mut crate::ProtoChunk,
_block_registry: &dyn crate::world::WorldPortalExt,
_random: &mut pumpkin_util::random::RandomGenerator,
random: &mut pumpkin_util::random::RandomGenerator,
_seed: i64,
chunk_box: &pumpkin_util::math::block_box::BlockBox,
) {
let origin =
pumpkin_util::math::vector3::Vector3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z);
let processors: Vec<Box<dyn crate::generation::structure::template::StructureProcessor>> =
vec![Box::new(
crate::generation::structure::template::processor::GravityProcessor {
heightmap: pumpkin_util::HeightMap::WorldSurfaceWg,
offset: -1,
},
)];
self.element
.for_each_template(|_name, processor_list, template| {
let processors = match processor_list {
ProcessorListRef::Named(name) => {
crate::generation::structure::template::processor::load_processor_list(name)
}
ProcessorListRef::Empty => Arc::from([]),
};
crate::generation::structure::template::place_template(
chunk,
&template,
origin,
(0, 0),
self.rotation,
false,
self.liquid_settings == LiquidSettings::ApplyWaterlog,
processors.as_ref(),
Some(chunk_box),
);
});
crate::generation::structure::template::place_template(
chunk,
&self.template,
origin,
(0, 0),
self.rotation,
false,
&processors,
Some(chunk_box),
);
// Post-process: replace jigsaw blocks with their final_state
for jigsaw in &self.jigsaw_blocks {
let wx = jigsaw.pos.0.x;
let wy = jigsaw.pos.0.y;
let wz = jigsaw.pos.0.z;
if wx < chunk_box.min.x
|| wx > chunk_box.max.x
|| wy < chunk_box.min.y
|| wy > chunk_box.max.y
|| wz < chunk_box.min.z
|| wz > chunk_box.max.z
{
continue;
}
if jigsaw.final_state == "minecraft:air" || jigsaw.final_state == "air" {
chunk.set_block_state(wx, wy, wz, pumpkin_data::Block::AIR.default_state);
continue;
}
let entry = PaletteEntry::from_string(&jigsaw.final_state);
if let Some(state) = BlockStateResolver::resolve(&entry, self.rotation, self.mirror) {
chunk.set_block_state(wx, wy, wz, state);
}
if let Some(feature) = self.element.feature()
&& let Some(placed_feature) =
crate::generation::feature::placed_features::PLACED_FEATURES.get(&feature)
{
placed_feature.generate_in_proto_chunk(chunk, feature, random, self.pos);
}
}
}
@@ -363,13 +596,123 @@ impl StructureGenerator for JigsawGenerator {
start_pos,
self.use_expansion_hack,
project_start_to_heightmap,
MaxDistance {
horizontal: max_distance,
vertical: max_distance,
},
MaxDistance::new(max_distance),
dimension_padding,
liquid_settings,
&PoolAliasLookup,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ancient_city_pools_match_vanilla_weights() {
let expected = [
("minecraft:ancient_city/city_center", 3, 3),
("minecraft:ancient_city/sculk", 2, 7),
("minecraft:ancient_city/structures", 20, 46),
("minecraft:ancient_city/walls", 16, 27),
("minecraft:ancient_city/city/entrance", 6, 6),
("minecraft:ancient_city/city_center/walls", 10, 10),
("minecraft:ancient_city/walls/no_corners", 8, 8),
];
for (id, element_count, total_weight) in expected {
let pool = TemplatePool::discover(id).unwrap_or_else(|| panic!("missing pool {id}"));
assert_eq!(pool.elements.len(), element_count, "{id}");
assert_eq!(
pool.elements
.iter()
.map(|element| element.weight)
.sum::<u32>(),
total_weight,
"{id}"
);
assert_eq!(pool.fallback, "minecraft:empty", "{id}");
}
}
#[test]
fn ancient_city_start_templates_and_anchor_exist() {
let pool = TemplatePool::discover("minecraft:ancient_city/city_center").unwrap();
for element in pool.elements {
let template = element.first_template().expect("missing start template");
assert!(
template.blocks.iter().any(|block| {
JigsawBlock::from_template_block(block, &template.palette[block.state as usize])
.is_some_and(|jigsaw| jigsaw.name == "minecraft:city_anchor")
}),
"start template has no city_anchor"
);
}
}
#[test]
fn ancient_city_pool_templates_are_embedded() {
fn check(kind: &PoolElementKind) {
match kind {
PoolElementKind::Single { template, .. } => {
// This entry exists in vanilla's pool data but has no corresponding
// template in the vanilla server jar.
if template == "minecraft:ancient_city/walls/intact_horizontal_wall_stairs_5" {
assert!(
crate::generation::structure::template::get_template(template)
.is_none()
);
} else {
assert!(
crate::generation::structure::template::get_template(template)
.is_some(),
"missing template {template}"
);
}
}
PoolElementKind::List(elements) => elements.iter().for_each(check),
PoolElementKind::Empty | PoolElementKind::Feature(_) => {}
}
}
for id in [
"minecraft:ancient_city/city_center",
"minecraft:ancient_city/structures",
"minecraft:ancient_city/walls",
"minecraft:ancient_city/city/entrance",
"minecraft:ancient_city/city_center/walls",
"minecraft:ancient_city/walls/no_corners",
] {
for element in TemplatePool::discover(id).unwrap().elements {
check(&element.kind);
}
}
}
#[test]
fn ancient_city_builds_a_multi_piece_graph() {
let generator = JigsawGenerator::new("minecraft:ancient_city/city_center", 7)
.with_start_jigsaw("minecraft:city_anchor");
let context = StructureGeneratorContext {
seed: 0,
chunk_x: 0,
chunk_z: 0,
random: super::super::create_chunk_random(0, 0, 0),
sea_level: 63,
min_y: -64,
height_sampler: None,
structure_key: Some(pumpkin_data::structures::StructureKeys::AncientCity),
};
let position = generator
.get_structure_position(context)
.expect("ancient city graph should generate");
let collector = position.collector.lock().unwrap();
assert!(
collector.pieces.len() > 10,
"ancient city generated only {} pieces",
collector.pieces.len()
);
assert_eq!(position.start_pos.0.y, -27);
}
}

View File

@@ -11,8 +11,8 @@ use std::collections::BinaryHeap;
use std::sync::Arc;
use super::jigsaw::{
JigsawBlock, JigsawJointType, JigsawJunction, JigsawProjection, PoolElementStructurePiece,
TemplatePool,
JigsawBlock, JigsawJointType, JigsawJunction, JigsawProjection, PoolElement, PoolElementKind,
PoolElementStructurePiece, TemplatePool,
};
pub struct JigsawPlacement;
@@ -86,8 +86,8 @@ impl JigsawPlacement {
let actual_start_pool_id = pool_alias_lookup.lookup(start_pool_id);
let pool = TemplatePool::discover(actual_start_pool_id)?;
let element = pool.get_random_element(&mut context.random);
let template = get_template(element.template)?;
let element = pool.get_random_element(&mut context.random).clone();
let template = element.first_template()?;
let rotation = Rotation::from_index(context.random.next_bounded_i32(4) as u8);
@@ -183,7 +183,7 @@ impl JigsawPlacement {
box_,
0,
),
template: Arc::clone(&template),
element: element.clone(),
pos: BlockPos::new(adjusted_position.0.x, bottom_y, adjusted_position.0.z),
rotation,
mirror: Mirror::None,
@@ -243,34 +243,25 @@ impl JigsawPlacement {
let mut target_elements = Vec::new();
if depth < max_depth {
let mut main_elements = target_pool.elements.clone();
for i in (1..main_elements.len()).rev() {
let j = context.random.next_bounded_i32(i as i32 + 1) as usize;
main_elements.swap(i, j);
}
target_elements.extend(main_elements);
target_elements
.extend(target_pool.get_shuffled_elements(&mut context.random));
}
let fallback_pool_id = pool_alias_lookup.lookup(&target_pool.fallback);
if let Some(fallback_pool) = TemplatePool::discover(fallback_pool_id) {
let mut fallback_elements = fallback_pool.elements.clone();
for i in (1..fallback_elements.len()).rev() {
let j = context.random.next_bounded_i32(i as i32 + 1) as usize;
fallback_elements.swap(i, j);
}
target_elements.extend(fallback_elements);
target_elements
.extend(fallback_pool.get_shuffled_elements(&mut context.random));
}
for element in target_elements {
if element.template == "minecraft:empty" {
if element.is_empty() {
break;
}
let target_template_arc = match get_template(element.template) {
Some(t) => t,
let target_size = match get_element_size(&element) {
Some(size) => size,
None => continue,
};
let target_template = target_template_arc.as_ref();
let target_projection = element.projection;
let target_rigid = target_projection == JigsawProjection::Rigid;
@@ -287,7 +278,7 @@ impl JigsawPlacement {
}
for target_rotation in rotations {
let target_jigsaws = get_jigsaw_blocks(target_template);
let target_jigsaws = get_element_jigsaw_blocks(&element);
let mut target_jigsaws_shuffled = target_jigsaws.clone();
for i in (1..target_jigsaws_shuffled.len()).rev() {
@@ -310,8 +301,8 @@ impl JigsawPlacement {
let source_jigsaw_local_y =
source_jigsaw_pos.0.y - source_box.min.y;
let target_jigsaw_local_pos = target_rotation
.transform_pos(target_jigsaw.pos.0, target_template.size);
let target_jigsaw_local_pos =
target_rotation.transform_pos(target_jigsaw.pos.0, target_size);
let target_jigsaw_local_y = target_jigsaw_local_pos.y;
let delta_y = source_jigsaw_local_y - target_jigsaw_local_y
@@ -354,7 +345,7 @@ impl JigsawPlacement {
target_pos.0.y += y_offset;
let rotated_target_size =
target_rotation.transform_size(target_template.size);
target_rotation.transform_size(target_size);
let mut target_box = BlockBox::new(
target_pos.0.x,
target_pos.0.y,
@@ -371,13 +362,13 @@ impl JigsawPlacement {
for tj in &target_jigsaws {
let tj_facing =
rotate_direction(tj.facing, target_rotation);
let rotated_tj_pos = target_rotation
.transform_pos(tj.pos.0, target_template.size);
let rotated_tj_pos =
target_rotation.transform_pos(tj.pos.0, target_size);
let rotated_tj_target_pos =
rotated_tj_pos.add(&tj_facing.to_vector());
let rotated_size =
target_rotation.transform_size(target_template.size);
target_rotation.transform_size(target_size);
let hack_box = BlockBox::new(
0,
0,
@@ -423,23 +414,17 @@ impl JigsawPlacement {
if !intersects_any(&pieces, &target_box) {
let mut child_jigsaw_blocks = Vec::new();
for block in &target_template.blocks {
if let Some(mut cj) = JigsawBlock::from_template_block(
block,
&target_template.palette[block.state as usize],
) {
let rotated_pos = target_rotation
.transform_pos(cj.pos.0, target_template.size);
cj.pos = BlockPos(rotated_pos).add(
target_pos.0.x,
target_pos.0.y,
target_pos.0.z,
);
cj.facing =
rotate_direction(cj.facing, target_rotation);
cj.up = rotate_direction(cj.up, target_rotation);
child_jigsaw_blocks.push(cj);
}
for mut cj in get_element_jigsaw_blocks(&element) {
let rotated_pos =
target_rotation.transform_pos(cj.pos.0, target_size);
cj.pos = BlockPos(rotated_pos).add(
target_pos.0.x,
target_pos.0.y,
target_pos.0.z,
);
cj.facing = rotate_direction(cj.facing, target_rotation);
cj.up = rotate_direction(cj.up, target_rotation);
child_jigsaw_blocks.push(cj);
}
let source_ground_level_delta =
@@ -456,7 +441,7 @@ impl JigsawPlacement {
target_box,
depth as u32 + 1,
),
template: target_template_arc.clone(),
element: element.clone(),
pos: target_pos,
rotation: target_rotation,
mirror: Mirror::None,
@@ -552,11 +537,11 @@ fn get_pool_max_y_size(pool_id: &str) -> i32 {
let mut max_y = 0;
for element in &pool.elements {
if element.template == "minecraft:empty" {
if element.is_empty() {
continue;
}
if let Some(template) = get_template(element.template) {
max_y = max_y.max(template.size.y);
if let Some(size) = get_element_size(element) {
max_y = max_y.max(size.y);
}
}
max_y
@@ -629,8 +614,60 @@ fn get_jigsaw_blocks(template: &StructureTemplate) -> Vec<JigsawBlock> {
jigsaws
}
fn get_element_size(element: &PoolElement) -> Option<pumpkin_util::math::vector3::Vector3<i32>> {
fn size_for_kind(kind: &PoolElementKind) -> Option<pumpkin_util::math::vector3::Vector3<i32>> {
use pumpkin_util::math::vector3::Vector3;
match kind {
PoolElementKind::Empty => None,
PoolElementKind::Feature(_) => Some(Vector3::new(1, 1, 1)),
PoolElementKind::Single { template, .. } => get_template(template).map(|t| t.size),
PoolElementKind::List(elements) => {
let mut result = Vector3::new(0, 0, 0);
let mut found = false;
for size in elements.iter().filter_map(size_for_kind) {
result.x = result.x.max(size.x);
result.y = result.y.max(size.y);
result.z = result.z.max(size.z);
found = true;
}
found.then_some(result)
}
}
}
size_for_kind(&element.kind)
}
fn get_element_jigsaw_blocks(element: &PoolElement) -> Vec<JigsawBlock> {
fn jigsaws_for_kind(kind: &PoolElementKind) -> Vec<JigsawBlock> {
match kind {
PoolElementKind::Single { template, .. } => get_template(template)
.map_or_else(Vec::new, |template| get_jigsaw_blocks(&template)),
PoolElementKind::List(elements) => {
elements.first().map_or_else(Vec::new, jigsaws_for_kind)
}
PoolElementKind::Feature(_) => vec![JigsawBlock {
pos: BlockPos::new(0, 0, 0),
name: "minecraft:bottom".to_string(),
target: "minecraft:empty".to_string(),
pool: "minecraft:empty".to_string(),
final_state: "minecraft:air".to_string(),
joint: JigsawJointType::Rollable,
facing: pumpkin_util::BlockDirection::Down,
up: pumpkin_util::BlockDirection::South,
selection_priority: 0,
placement_priority: 0,
}],
PoolElementKind::Empty => Vec::new(),
}
}
jigsaws_for_kind(&element.kind)
}
fn can_attach(source: &JigsawBlock, target: &JigsawBlock, target_rotation: Rotation) -> bool {
if source.target != target.name || target.target != source.name {
if source.target != target.name {
return false;
}
let rotated_target_facing = rotate_direction(target.facing, target_rotation);
@@ -638,11 +675,7 @@ fn can_attach(source: &JigsawBlock, target: &JigsawBlock, target_rotation: Rotat
return false;
}
// Joint alignment for vertical connections
if (source.joint == JigsawJointType::Aligned || target.joint == JigsawJointType::Aligned)
&& (source.facing == pumpkin_util::BlockDirection::Up
|| source.facing == pumpkin_util::BlockDirection::Down)
{
if source.joint == JigsawJointType::Aligned {
let rotated_target_up = rotate_direction(target.up, target_rotation);
return source.up == rotated_target_up;
}

View File

@@ -7,7 +7,7 @@ use pumpkin_util::HeightMap;
use pumpkin_util::{
BlockDirection,
math::{block_box::BlockBox, position::BlockPos, vector3::Vector3},
random::{RandomGenerator, RandomImpl, get_carver_seed, xoroshiro128::Xoroshiro},
random::{RandomGenerator, RandomImpl, legacy_rand::LegacyRand},
};
use tracing::trace;
@@ -685,8 +685,13 @@ pub struct StructureGeneratorContext<'a> {
#[must_use]
pub fn create_chunk_random(seed: i64, chunk_x: i32, chunk_z: i32) -> RandomGenerator {
let carver_seed = get_carver_seed(seed as u64, chunk_x, chunk_z);
RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed))
let mut seeder = LegacyRand::from_seed(seed as u64);
let x_multiplier = seeder.next_i64();
let z_multiplier = seeder.next_i64();
let structure_seed = (i64::from(chunk_x).wrapping_mul(x_multiplier))
^ (i64::from(chunk_z).wrapping_mul(z_multiplier))
^ seed;
RandomGenerator::Legacy(LegacyRand::from_seed(structure_seed as u64))
}
pub enum StructureInstance {
@@ -696,3 +701,29 @@ pub enum StructureInstance {
/// Stores the `BlockPos` of the 'Start' so you can look it up.
Reference(Arc<Mutex<StructurePiecesCollector>>),
}
#[cfg(test)]
mod structure_random_tests {
use super::*;
#[test]
fn large_feature_seed_matches_java_random() {
let mut random = create_chunk_random(123_456_789, -37, 84);
assert_eq!(
[
random.next_i32(),
random.next_i32(),
random.next_i32(),
random.next_i32(),
random.next_i32(),
],
[
-2_113_851_872,
-821_770_162,
381_681_559,
-196_012_664,
372_718_864
]
);
}
}

View File

@@ -188,6 +188,7 @@ impl StructurePieceBase for NetherFossilPiece {
(0, 0),
self.rotation,
true,
false,
&[],
Some(_chunk_box),
);

View File

@@ -46,17 +46,26 @@ impl BlockStateResolver {
}
// Transform properties for rotation/mirror
let transformed_props: Vec<(&str, &str)> = entry
let transformed_props: Vec<(String, String)> = entry
.properties
.iter()
.map(|(key, value)| {
let transformed_key = match key.as_str() {
"north" | "south" | "east" | "west" => rotation
.rotate_facing(mirror.mirror_facing(key))
.to_string(),
_ => key.clone(),
};
let new_value = Self::transform_property(key, value, rotation, mirror);
(key.as_str(), new_value)
(transformed_key, new_value)
})
.collect();
// Convert to the format expected by from_properties
let props_slice: Vec<(&str, &str)> = transformed_props;
let props_slice = transformed_props
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
.collect::<Vec<_>>();
// Get the state ID from properties
let props_box = block.from_properties(&props_slice);
@@ -72,21 +81,16 @@ impl BlockStateResolver {
}
/// Transforms a property value based on rotation and mirror.
fn transform_property(
key: &str,
value: &str,
rotation: Rotation,
mirror: Mirror,
) -> &'static str {
fn transform_property(key: &str, value: &str, rotation: Rotation, mirror: Mirror) -> String {
match key {
// Horizontal facing properties
"facing" => {
let mirrored = mirror.mirror_facing(value);
rotation.rotate_facing(mirrored)
rotation.rotate_facing(mirrored).to_string()
}
// Axis properties (for logs, pillars, etc.)
"axis" => rotation.rotate_axis(value),
"axis" => rotation.rotate_axis(value).to_string(),
// Block rotation (signs, banners - 0-15 value)
"rotation" => {
@@ -94,15 +98,15 @@ impl BlockStateResolver {
let mirrored = mirror.mirror_block_rotation(rot_value);
let rotated = rotation.rotate_block_rotation(mirrored);
// Use static strings for the 16 possible rotation values
Self::rotation_to_str(rotated)
Self::rotation_to_str(rotated).to_string()
} else {
Self::leak_str(value)
value.to_string()
}
}
// Half properties don't need rotation (top/bottom stays the same)
// Shape, mode, and most other properties don't need transformation either
_ => Self::leak_str(value),
_ => value.to_string(),
}
}
@@ -128,56 +132,6 @@ impl BlockStateResolver {
_ => "0",
}
}
/// Converts a string to a 'static str by leaking it.
/// This is necessary for returning transformed property values.
fn leak_str(s: &str) -> &'static str {
// For common values, return static strings to avoid leaking
match s {
"north" => "north",
"south" => "south",
"east" => "east",
"west" => "west",
"up" => "up",
"down" => "down",
"x" => "x",
"y" => "y",
"z" => "z",
"true" => "true",
"false" => "false",
"top" => "top",
"bottom" => "bottom",
"upper" => "upper",
"lower" => "lower",
"straight" => "straight",
"inner_left" => "inner_left",
"inner_right" => "inner_right",
"outer_left" => "outer_left",
"outer_right" => "outer_right",
"0" => "0",
"1" => "1",
"2" => "2",
"3" => "3",
"4" => "4",
"5" => "5",
"6" => "6",
"7" => "7",
"8" => "8",
"9" => "9",
"10" => "10",
"11" => "11",
"12" => "12",
"13" => "13",
"14" => "14",
"15" => "15",
"head" => "head",
"foot" => "foot",
"single" => "single",
"left" => "left",
"right" => "right",
_ => s.to_string().leak(),
}
}
}
#[cfg(test)]

View File

@@ -14,7 +14,7 @@ use super::{StructureTemplate, structure_template::TemplateError};
/// Templates are loaded lazily on first access and stored for reuse.
/// The cache is thread-safe and can be accessed from multiple threads.
pub struct TemplateCache {
cache: DashMap<&'static str, Arc<StructureTemplate>>,
cache: DashMap<String, Arc<StructureTemplate>>,
}
impl Default for TemplateCache {
@@ -36,7 +36,9 @@ impl TemplateCache {
///
/// Returns the loaded template wrapped in an `Arc`, or `None` if the template
/// doesn't exist or failed to load.
pub fn get(&self, name: &'static str) -> Option<Arc<StructureTemplate>> {
pub fn get(&self, name: &str) -> Option<Arc<StructureTemplate>> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
// Check cache first
if let Some(template) = self.cache.get(name) {
return Some(Arc::clone(&template));
@@ -48,7 +50,7 @@ impl TemplateCache {
match StructureTemplate::from_nbt_bytes(bytes) {
Ok(template) => {
let arc = Arc::new(template);
self.cache.insert(name, Arc::clone(&arc));
self.cache.insert(name.to_owned(), Arc::clone(&arc));
Some(arc)
}
Err(e) => {
@@ -63,10 +65,9 @@ impl TemplateCache {
/// # Errors
///
/// Returns an error if the template doesn't exist or fails to parse.
pub fn get_or_error(
&self,
name: &'static str,
) -> Result<Arc<StructureTemplate>, TemplateError> {
pub fn get_or_error(&self, name: &str) -> Result<Arc<StructureTemplate>, TemplateError> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
// Check cache first
if let Some(template) = self.cache.get(name) {
return Ok(Arc::clone(&template));
@@ -78,7 +79,7 @@ impl TemplateCache {
let template = StructureTemplate::from_nbt_bytes(bytes)?;
let arc = Arc::new(template);
self.cache.insert(name, Arc::clone(&arc));
self.cache.insert(name.to_owned(), Arc::clone(&arc));
Ok(arc)
}
@@ -139,6 +140,6 @@ pub fn global_cache() -> &'static TemplateCache {
///
/// Returns the loaded template wrapped in an `Arc`, or `None` if not found.
#[must_use]
pub fn get_template(name: &'static str) -> Option<Arc<StructureTemplate>> {
pub fn get_template(name: &str) -> Option<Arc<StructureTemplate>> {
global_cache().get(name)
}

View File

@@ -33,11 +33,15 @@ mod template_piece;
use pumpkin_data::Rotation;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::random::{RandomImpl, hash_block_pos, legacy_rand::LegacyRand};
use crate::ProtoChunk;
pub use block_state_resolver::BlockStateResolver;
pub use cache::{TemplateCache, get_pool_elements, get_template, global_cache};
pub use cache::{
TemplateCache, get_pool_elements, get_processor_list_json, get_template,
get_template_pool_json, global_cache,
};
pub use processor::StructureProcessor;
pub use pumpkin_data::{Mirror as BlockMirror, Rotation as BlockRotation};
pub use structure_template::{PaletteEntry, StructureTemplate, TemplateBlock, TemplateEntity};
@@ -61,7 +65,8 @@ pub fn place_template(
offset: (i32, i32),
rotation: Rotation,
skip_air: bool,
processors: &[Box<dyn StructureProcessor>],
apply_waterlogging: bool,
processors: &[StructureProcessor],
chunk_box: Option<&pumpkin_util::math::block_box::BlockBox>,
) {
let (rotated_ox, rotated_oz) = rotation.rotate_offset(offset.0, offset.1);
@@ -71,8 +76,10 @@ pub fn place_template(
for block in &template.blocks {
let palette_entry = &template.palette[block.state as usize];
// Skip structure void blocks
if palette_entry.name == "minecraft:structure_void" {
// Structure blocks are data markers and structure void preserves the existing block.
if palette_entry.name == "minecraft:structure_void"
|| palette_entry.name == "minecraft:structure_block"
{
continue;
}
@@ -81,9 +88,23 @@ pub fn place_template(
continue;
}
let mut block_entity_nbt = block.nbt.clone();
let mut placed_entry = palette_entry.clone();
// Jigsaw blocks are replaced during template processing, before block entities are
// collected. Keeping this in the placement pipeline avoids stale jigsaw entities.
if palette_entry.name == "minecraft:jigsaw" {
let final_state = block_entity_nbt
.as_ref()
.and_then(|nbt| nbt.get_string("final_state"))
.unwrap_or("minecraft:air");
placed_entry = PaletteEntry::from_string(final_state);
block_entity_nbt = None;
}
// Resolve block state with rotation applied to directional properties
let Some(mut state) =
BlockStateResolver::resolve(palette_entry, rotation, Default::default())
BlockStateResolver::resolve(&placed_entry, rotation, Default::default())
else {
continue;
};
@@ -108,39 +129,67 @@ pub fn place_template(
let world_pos = Vector3::new(wx, wy, wz);
if apply_waterlogging
&& chunk.get_block_state(&world_pos).to_block_id() == pumpkin_data::Block::WATER.id
&& let Some((_, waterlogged)) = placed_entry
.properties
.iter_mut()
.find(|(name, _)| name == "waterlogged")
{
*waterlogged = "true".to_string();
if let Some(waterlogged_state) =
BlockStateResolver::resolve(&placed_entry, rotation, Default::default())
{
state = waterlogged_state;
}
}
// Apply processors
let mut should_place = true;
for processor in processors {
state = processor.process(chunk, world_pos, state);
let Some(processed_state) = processor.process(chunk, world_pos, state) else {
should_place = false;
break;
};
state = processed_state;
}
if !should_place {
continue;
}
chunk.set_block_state(wx, wy, wz, state);
// Create block entities for interactive blocks (furnaces, chests, etc.)
let block_entity_id = get_block_entity_id(&palette_entry.name);
if block.nbt.is_some() || block_entity_id.is_some() {
let block_entity_id = block_entity_id.unwrap_or(&palette_entry.name);
let mut block_entity_nbt = NbtCompound::new();
let block_entity_id = get_block_entity_id(&placed_entry.name);
if block_entity_nbt.is_some() || block_entity_id.is_some() {
let block_entity_id = block_entity_id.unwrap_or(&placed_entry.name);
let mut placed_nbt = NbtCompound::new();
block_entity_nbt.put_string("id", block_entity_id.to_string());
block_entity_nbt.put_int("x", wx);
block_entity_nbt.put_int("y", wy);
block_entity_nbt.put_int("z", wz);
placed_nbt.put_string("id", block_entity_id.to_string());
placed_nbt.put_int("x", wx);
placed_nbt.put_int("y", wy);
placed_nbt.put_int("z", wz);
if let Some(template_nbt) = &block.nbt {
if let Some(template_nbt) = &block_entity_nbt {
for (key, value) in &template_nbt.child_tags {
if key.as_ref() != "x"
&& key.as_ref() != "y"
&& key.as_ref() != "z"
&& key.as_ref() != "id"
{
block_entity_nbt
.child_tags
.insert(key.clone(), value.clone());
placed_nbt.child_tags.insert(key.clone(), value.clone());
}
}
}
chunk.add_block_entity(block_entity_nbt);
if placed_nbt.get_string("LootTable").is_some()
&& placed_nbt.get_long("LootTableSeed").is_none()
{
let mut random = LegacyRand::from_seed(hash_block_pos(wx, wy, wz) as u64);
placed_nbt.put_long("LootTableSeed", random.next_i64());
}
chunk.add_block_entity(placed_nbt);
}
}
}

View File

@@ -1,43 +1,209 @@
use pumpkin_data::{Block, BlockState};
use pumpkin_util::{
math::vector3::Vector3,
random::{RandomImpl, hash_block_pos, legacy_rand::LegacyRand},
};
use serde::Deserialize;
use std::sync::{Arc, LazyLock};
use crate::ProtoChunk;
use pumpkin_data::BlockState;
use pumpkin_util::math::vector3::Vector3;
pub trait StructureProcessor: Send + Sync {
fn process(
&self,
chunk: &mut ProtoChunk,
pos: Vector3<i32>,
state: &'static BlockState,
) -> &'static BlockState;
#[derive(Clone)]
pub enum StructureProcessor {
BlockRot { integrity: f32, blocks: BlockTag },
Rules(Vec<ProcessorRule>),
ProtectedBlocks(BlockTag),
}
pub struct GravityProcessor {
pub heightmap: pumpkin_util::HeightMap,
pub offset: i32,
#[derive(Clone)]
pub struct ProcessorRule {
input_block: u16,
probability: f32,
output_state: &'static BlockState,
}
impl StructureProcessor for GravityProcessor {
fn process(
&self,
chunk: &mut ProtoChunk,
pos: Vector3<i32>,
state: &'static BlockState,
) -> &'static BlockState {
let top_y = chunk.get_top_y(&self.heightmap, pos.x, pos.z);
let _target_y = top_y + self.offset;
#[derive(Clone, Copy)]
pub enum BlockTag {
AncientCityReplaceable,
FeaturesCannotReplace,
}
// For now, just a simple gravity: if we are placing above the ground, shift it down?
// Actually, GravityProcessor in vanilla is used to place blocks relative to the ground.
// But our jigsaw already adjusted the piece origin.
impl BlockTag {
fn from_name(name: &str) -> Option<Self> {
match name {
"#minecraft:ancient_city_replaceable" => Some(Self::AncientCityReplaceable),
"#minecraft:features_cannot_replace" => Some(Self::FeaturesCannotReplace),
_ => None,
}
}
let block = pumpkin_data::Block::from_state_id(state.id);
if block.name == "cobblestone" || block.name == "stone_bricks" {
// Fill down to ground
for y in (top_y..pos.y).rev() {
chunk.set_block_state(pos.x, y, pos.z, state);
fn contains(self, block_id: u16) -> bool {
match self {
Self::AncientCityReplaceable => {
pumpkin_data::tag::Block::MINECRAFT_ANCIENT_CITY_REPLACEABLE
.1
.contains(&block_id)
}
Self::FeaturesCannotReplace => {
pumpkin_data::tag::Block::MINECRAFT_FEATURES_CANNOT_REPLACE
.1
.contains(&block_id)
}
}
}
}
state
impl StructureProcessor {
#[must_use]
pub fn process(
&self,
chunk: &ProtoChunk,
pos: Vector3<i32>,
state: &'static BlockState,
) -> Option<&'static BlockState> {
let input_block = Block::get_raw_id_from_state_id(state.id);
match self {
Self::BlockRot { integrity, blocks } => {
if !blocks.contains(input_block) {
return Some(state);
}
let mut random = LegacyRand::from_seed(hash_block_pos(pos.x, pos.y, pos.z) as u64);
(random.next_f32() <= *integrity).then_some(state)
}
Self::Rules(rules) => {
let mut random = LegacyRand::from_seed(hash_block_pos(pos.x, pos.y, pos.z) as u64);
rules
.iter()
.find(|rule| {
input_block == rule.input_block && random.next_f32() < rule.probability
})
.map_or(Some(state), |rule| Some(rule.output_state))
}
Self::ProtectedBlocks(blocks) => {
let existing = chunk.get_block_state(&pos).to_block_id();
(!blocks.contains(existing)).then_some(state)
}
}
}
}
#[derive(Deserialize)]
struct RawProcessorList {
processors: Vec<RawProcessor>,
}
#[derive(Deserialize)]
#[serde(tag = "processor_type")]
enum RawProcessor {
#[serde(rename = "minecraft:block_rot")]
BlockRot {
integrity: f32,
rottable_blocks: String,
},
#[serde(rename = "minecraft:rule")]
Rule { rules: Vec<RawRule> },
#[serde(rename = "minecraft:protected_blocks")]
ProtectedBlocks { value: String },
}
#[derive(Deserialize)]
struct RawRule {
input_predicate: RawInputPredicate,
output_state: RawOutputState,
}
#[derive(Deserialize)]
struct RawInputPredicate {
block: String,
probability: f32,
}
#[derive(Deserialize)]
struct RawOutputState {
#[serde(rename = "Name")]
name: String,
}
#[must_use]
pub fn load_processor_list(name: &str) -> Arc<[StructureProcessor]> {
static CACHE: LazyLock<dashmap::DashMap<String, Arc<[StructureProcessor]>>> =
LazyLock::new(dashmap::DashMap::new);
if let Some(processors) = CACHE.get(name) {
return Arc::clone(&processors);
}
let Some(json) = super::cache::get_processor_list_json(name) else {
tracing::warn!("Unknown structure processor list: {name}");
return Arc::from([]);
};
let raw: RawProcessorList = match serde_json::from_str(json) {
Ok(raw) => raw,
Err(error) => {
tracing::error!("Failed to parse structure processor list {name}: {error}");
return Arc::from([]);
}
};
let processors = raw
.processors
.into_iter()
.filter_map(|processor| match processor {
RawProcessor::BlockRot {
integrity,
rottable_blocks,
} => BlockTag::from_name(&rottable_blocks)
.map(|blocks| StructureProcessor::BlockRot { integrity, blocks }),
RawProcessor::ProtectedBlocks { value } => {
BlockTag::from_name(&value).map(StructureProcessor::ProtectedBlocks)
}
RawProcessor::Rule { rules } => Some(StructureProcessor::Rules(
rules
.into_iter()
.filter_map(|rule| {
let input_name = rule
.input_predicate
.block
.strip_prefix("minecraft:")
.unwrap_or(&rule.input_predicate.block);
let output_name = rule
.output_state
.name
.strip_prefix("minecraft:")
.unwrap_or(&rule.output_state.name);
let input_block = Block::from_name(input_name)?;
let output_block = Block::from_name(output_name)?;
Some(ProcessorRule {
input_block: input_block.id,
probability: rule.input_predicate.probability,
output_state: output_block.default_state,
})
})
.collect(),
)),
})
.collect::<Arc<[_]>>();
CACHE.insert(name.to_owned(), Arc::clone(&processors));
processors
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_ancient_city_processor_lists() {
assert_eq!(
load_processor_list("minecraft:ancient_city_generic_degradation").len(),
3
);
assert_eq!(
load_processor_list("minecraft:ancient_city_start_degradation").len(),
2
);
assert_eq!(
load_processor_list("minecraft:ancient_city_walls_degradation").len(),
3
);
}
}

View File

@@ -113,28 +113,32 @@ impl JigsawBlockEntity {
let mut pieces = std::mem::take(&mut structure.collector.lock().unwrap().pieces);
for piece in &mut pieces {
if let Some(pool_piece) = piece.as_any().downcast_ref::<PoolElementStructurePiece>() {
let template = Arc::clone(&pool_piece.template);
let origin = pool_piece.pos;
let rotation = pool_piece.rotation;
let mut templates = Vec::new();
pool_piece
.element
.for_each_template(|_, _, template| templates.push(template));
for block in &template.blocks {
let palette_entry = &template.palette[block.state as usize];
if palette_entry.name == "minecraft:structure_void" {
continue;
}
for template in templates {
for block in &template.blocks {
let palette_entry = &template.palette[block.state as usize];
if palette_entry.name == "minecraft:structure_void" {
continue;
}
if !keep_jigsaws && palette_entry.name == "minecraft:jigsaw" {
let final_state_str = block
.nbt
.as_ref()
.and_then(|n| n.get_string("final_state"))
.unwrap_or("minecraft:air");
if !keep_jigsaws && palette_entry.name == "minecraft:jigsaw" {
let final_state_str = block
.nbt
.as_ref()
.and_then(|n| n.get_string("final_state"))
.unwrap_or("minecraft:air");
let entry =
let entry =
pumpkin_world::generation::structure::template::PaletteEntry::from_string(
final_state_str,
);
let final_state =
let final_state =
pumpkin_world::generation::structure::template::BlockStateResolver::resolve(
&entry,
rotation,
@@ -142,48 +146,49 @@ impl JigsawBlockEntity {
)
.unwrap_or(pumpkin_data::Block::AIR.default_state);
let local_pos = rotation.transform_pos(block.pos, template.size);
let world_pos = origin.add(local_pos.x, local_pos.y, local_pos.z);
world
.set_block_state(
&world_pos,
final_state.id,
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
)
.await;
continue;
}
let Some(state) = BlockStateResolver::resolve(
palette_entry,
rotation,
pumpkin_data::Mirror::default(),
) else {
continue;
};
let local_pos = rotation.transform_pos(block.pos, template.size);
let world_pos = origin.add(local_pos.x, local_pos.y, local_pos.z);
world
.set_block_state(
&world_pos,
final_state.id,
state.id,
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
)
.await;
continue;
}
let Some(state) = BlockStateResolver::resolve(
palette_entry,
rotation,
pumpkin_data::Mirror::default(),
) else {
continue;
};
let local_pos = rotation.transform_pos(block.pos, template.size);
let world_pos = origin.add(local_pos.x, local_pos.y, local_pos.z);
world
.set_block_state(
&world_pos,
state.id,
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
)
.await;
// Handle block entities
if let Some(nbt) = &block.nbt {
let mut block_entity_nbt = nbt.clone();
block_entity_nbt.put_int("x", world_pos.0.x);
block_entity_nbt.put_int("y", world_pos.0.y);
block_entity_nbt.put_int("z", world_pos.0.z);
if let Some(block_entity) =
crate::block::entities::block_entity_from_nbt(&block_entity_nbt)
{
world.add_block_entity(block_entity);
// Handle block entities
if let Some(nbt) = &block.nbt {
let mut block_entity_nbt = nbt.clone();
block_entity_nbt.put_int("x", world_pos.0.x);
block_entity_nbt.put_int("y", world_pos.0.y);
block_entity_nbt.put_int("z", world_pos.0.z);
if let Some(block_entity) =
crate::block::entities::block_entity_from_nbt(&block_entity_nbt)
{
world.add_block_entity(block_entity);
}
}
}
}