diff --git a/Cargo.lock b/Cargo.lock index e6346de02..253e8d21a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2876,6 +2876,7 @@ dependencies = [ "rustc-hash", "ruzstd", "serde", + "serde_json", "serde_json5", "sha2 0.11.0", "slotmap", diff --git a/pumpkin-codegen/src/structures.rs b/pumpkin-codegen/src/structures.rs index c3917634a..c24ed9e45 100644 --- a/pumpkin-codegen/src/structures.rs +++ b/pumpkin-codegen/src/structures.rs @@ -81,6 +81,8 @@ pub struct StructureStruct { pub step: String, /// Optional jigsaw start pool. pub start_pool: Option, + /// Optional named jigsaw in the start pool that must be placed at the start position. + pub start_jigsaw_name: Option, /// Optional jigsaw size (depth). pub size: Option, /// 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, pub terrain_adaptation: TerrainAdaptation, pub start_height: Option, diff --git a/pumpkin-data/src/generated/structures.rs b/pumpkin-data/src/generated/structures.rs index 878f723be..311a11034 100644 --- a/pumpkin-data/src/generated/structures.rs +++ b/pumpkin-data/src/generated/structures.rs @@ -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, pub terrain_adaptation: TerrainAdaptation, pub start_height: Option, @@ -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), diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index d8e4093e3..eb309f626 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -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 diff --git a/pumpkin-world/assets/worldgen/processor_list/ancient_city_generic_degradation.json b/pumpkin-world/assets/worldgen/processor_list/ancient_city_generic_degradation.json new file mode 100644 index 000000000..9fd40f47d --- /dev/null +++ b/pumpkin-world/assets/worldgen/processor_list/ancient_city_generic_degradation.json @@ -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"}]} diff --git a/pumpkin-world/assets/worldgen/processor_list/ancient_city_start_degradation.json b/pumpkin-world/assets/worldgen/processor_list/ancient_city_start_degradation.json new file mode 100644 index 000000000..1f6b3f0f1 --- /dev/null +++ b/pumpkin-world/assets/worldgen/processor_list/ancient_city_start_degradation.json @@ -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"}]} diff --git a/pumpkin-world/assets/worldgen/processor_list/ancient_city_walls_degradation.json b/pumpkin-world/assets/worldgen/processor_list/ancient_city_walls_degradation.json new file mode 100644 index 000000000..0c0635258 --- /dev/null +++ b/pumpkin-world/assets/worldgen/processor_list/ancient_city_walls_degradation.json @@ -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"}]} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/city/entrance.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/city/entrance.json new file mode 100644 index 000000000..2bd61d447 --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/city/entrance.json @@ -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"} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/city_center.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/city_center.json new file mode 100644 index 000000000..5ea761b63 --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/city_center.json @@ -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"} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/city_center/walls.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/city_center/walls.json new file mode 100644 index 000000000..128b5d9f9 --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/city_center/walls.json @@ -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"} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/sculk.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/sculk.json new file mode 100644 index 000000000..b9d901cea --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/sculk.json @@ -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"} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/structures.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/structures.json new file mode 100644 index 000000000..d23d6766b --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/structures.json @@ -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"} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/walls.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/walls.json new file mode 100644 index 000000000..1abbdf295 --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/walls.json @@ -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"} diff --git a/pumpkin-world/assets/worldgen/template_pool/ancient_city/walls/no_corners.json b/pumpkin-world/assets/worldgen/template_pool/ancient_city/walls/no_corners.json new file mode 100644 index 000000000..0bbfa67cf --- /dev/null +++ b/pumpkin-world/assets/worldgen/template_pool/ancient_city/walls/no_corners.json @@ -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"} diff --git a/pumpkin-world/build.rs b/pumpkin-world/build.rs index f8fa3b0e9..0b4ac8bde 100644 --- a/pumpkin-world/build.rs +++ b/pumpkin-world/build.rs @@ -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::>(); + 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() + )); + } + } +} diff --git a/pumpkin-world/src/chunk_system/generation.rs b/pumpkin-world/src/chunk_system/generation.rs index 23c468bf6..39acbcdd7 100644 --- a/pumpkin-world/src/chunk_system/generation.rs +++ b/pumpkin-world/src/chunk_system/generation.rs @@ -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"); + } } diff --git a/pumpkin-world/src/chunk_system/schedule.rs b/pumpkin-world/src/chunk_system/schedule.rs index a4ff7cb2f..2d078dab6 100644 --- a/pumpkin-world/src/chunk_system/schedule.rs +++ b/pumpkin-world/src/chunk_system/schedule.rs @@ -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(); diff --git a/pumpkin-world/src/generation/feature/features/sculk_patch.rs b/pumpkin-world/src/generation/feature/features/sculk_patch.rs index 803c7c304..e5b40acd0 100644 --- a/pumpkin-world/src/generation/feature/features/sculk_patch.rs +++ b/pumpkin-world/src/generation/feature/features/sculk_patch.rs @@ -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 { + ((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(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()) + }) +} diff --git a/pumpkin-world/src/generation/feature/placed_features.rs b/pumpkin-world/src/generation/feature/placed_features.rs index 30f07b327..9de057f2b 100644 --- a/pumpkin-world/src/generation/feature/placed_features.rs +++ b/pumpkin-world/src/generation/feature/placed_features.rs @@ -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( &self, diff --git a/pumpkin-world/src/generation/structure/mod.rs b/pumpkin-world/src/generation/structure/mod.rs index 4fc9c66c6..6a94956da 100644 --- a/pumpkin-world/src/generation/structure/mod.rs +++ b/pumpkin-world/src/generation/structure/mod.rs @@ -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 diff --git a/pumpkin-world/src/generation/structure/structures/igloo.rs b/pumpkin-world/src/generation/structure/structures/igloo.rs index 3091ded8e..dadd1380e 100644 --- a/pumpkin-world/src/generation/structure/structures/igloo.rs +++ b/pumpkin-world/src/generation/structure/structures/igloo.rs @@ -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), ); diff --git a/pumpkin-world/src/generation/structure/structures/jigsaw.rs b/pumpkin-world/src/generation/structure/structures/jigsaw.rs index d2292b0a2..2ef1cd070 100644 --- a/pumpkin-world/src/generation/structure/structures/jigsaw.rs +++ b/pumpkin-world/src/generation/structure/structures/jigsaw.rs @@ -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), + Feature(pumpkin_data::placed_feature::PlacedFeature), +} + +#[derive(Clone, Default)] +pub enum ProcessorListRef { + Named(String), + #[default] + Empty, +} + +#[derive(Deserialize)] +struct RawTemplatePool { + fallback: String, + elements: Vec, +} + +#[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, + projection: RawProjection, + }, + #[serde(rename = "minecraft:feature_pool_element")] + Feature { + feature: String, + projection: RawProjection, + }, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum RawProcessorList { + Named(String), + Inline { processors: Vec }, +} + +#[derive(Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +enum RawProjection { + Rigid, + TerrainMatching, +} + +impl From 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> { + fn find(kind: &PoolElementKind) -> Option> { + 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), + ) { + fn visit( + kind: &PoolElementKind, + consumer: &mut impl FnMut(&str, &ProcessorListRef, Arc), + ) { + 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 { + 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 { - let elements = crate::generation::structure::template::get_pool_elements(id)?; + static CACHE: std::sync::LazyLock> = + 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 { + let mut elements = self + .elements + .iter() + .flat_map(|element| std::iter::repeat_n(element.clone(), element.weight as usize)) + .collect::>(); + 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, + 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> = - 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::(), + 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); + } +} diff --git a/pumpkin-world/src/generation/structure/structures/jigsaw_placement.rs b/pumpkin-world/src/generation/structure/structures/jigsaw_placement.rs index f2cf8529a..727c2537d 100644 --- a/pumpkin-world/src/generation/structure/structures/jigsaw_placement.rs +++ b/pumpkin-world/src/generation/structure/structures/jigsaw_placement.rs @@ -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 { jigsaws } +fn get_element_size(element: &PoolElement) -> Option> { + fn size_for_kind(kind: &PoolElementKind) -> Option> { + 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 { + fn jigsaws_for_kind(kind: &PoolElementKind) -> Vec { + 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; } diff --git a/pumpkin-world/src/generation/structure/structures/mod.rs b/pumpkin-world/src/generation/structure/structures/mod.rs index f8f74f891..63417c35d 100644 --- a/pumpkin-world/src/generation/structure/structures/mod.rs +++ b/pumpkin-world/src/generation/structure/structures/mod.rs @@ -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>), } + +#[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 + ] + ); + } +} diff --git a/pumpkin-world/src/generation/structure/structures/nether_fossil.rs b/pumpkin-world/src/generation/structure/structures/nether_fossil.rs index 61049d903..8fcfc38ae 100644 --- a/pumpkin-world/src/generation/structure/structures/nether_fossil.rs +++ b/pumpkin-world/src/generation/structure/structures/nether_fossil.rs @@ -188,6 +188,7 @@ impl StructurePieceBase for NetherFossilPiece { (0, 0), self.rotation, true, + false, &[], Some(_chunk_box), ); diff --git a/pumpkin-world/src/generation/structure/template/block_state_resolver.rs b/pumpkin-world/src/generation/structure/template/block_state_resolver.rs index 1fecd2e8f..fcd167643 100644 --- a/pumpkin-world/src/generation/structure/template/block_state_resolver.rs +++ b/pumpkin-world/src/generation/structure/template/block_state_resolver.rs @@ -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::>(); // 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)] diff --git a/pumpkin-world/src/generation/structure/template/cache.rs b/pumpkin-world/src/generation/structure/template/cache.rs index 99c59b50d..594492066 100644 --- a/pumpkin-world/src/generation/structure/template/cache.rs +++ b/pumpkin-world/src/generation/structure/template/cache.rs @@ -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>, + cache: DashMap>, } 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> { + pub fn get(&self, name: &str) -> Option> { + 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, TemplateError> { + pub fn get_or_error(&self, name: &str) -> Result, 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> { +pub fn get_template(name: &str) -> Option> { global_cache().get(name) } diff --git a/pumpkin-world/src/generation/structure/template/mod.rs b/pumpkin-world/src/generation/structure/template/mod.rs index 21a15feee..f6a9f4efa 100644 --- a/pumpkin-world/src/generation/structure/template/mod.rs +++ b/pumpkin-world/src/generation/structure/template/mod.rs @@ -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], + 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); } } } diff --git a/pumpkin-world/src/generation/structure/template/processor.rs b/pumpkin-world/src/generation/structure/template/processor.rs index 1069a0d1a..3a8f5c64c 100644 --- a/pumpkin-world/src/generation/structure/template/processor.rs +++ b/pumpkin-world/src/generation/structure/template/processor.rs @@ -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, - state: &'static BlockState, - ) -> &'static BlockState; +#[derive(Clone)] +pub enum StructureProcessor { + BlockRot { integrity: f32, blocks: BlockTag }, + Rules(Vec), + 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, - 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 { + 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, + 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, +} + +#[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 }, + #[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>> = + 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::>(); + 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 + ); } } diff --git a/pumpkin/src/block/entities/jigsaw_block.rs b/pumpkin/src/block/entities/jigsaw_block.rs index 081449127..e82c4d3d6 100644 --- a/pumpkin/src/block/entities/jigsaw_block.rs +++ b/pumpkin/src/block/entities/jigsaw_block.rs @@ -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::() { - 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); + } } } }