mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(generation): implement ancient city jigsaw structures (#2329)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2876,6 +2876,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"ruzstd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json5",
|
||||
"sha2 0.11.0",
|
||||
"slotmap",
|
||||
|
||||
@@ -81,6 +81,8 @@ pub struct StructureStruct {
|
||||
pub step: String,
|
||||
/// Optional jigsaw start pool.
|
||||
pub start_pool: Option<String>,
|
||||
/// Optional named jigsaw in the start pool that must be placed at the start position.
|
||||
pub start_jigsaw_name: Option<String>,
|
||||
/// Optional jigsaw size (depth).
|
||||
pub size: Option<i32>,
|
||||
/// Optional terrain adaptation (bearding).
|
||||
@@ -222,6 +224,11 @@ impl ToTokens for StructureStruct {
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
let start_jigsaw_name = if let Some(name) = &self.start_jigsaw_name {
|
||||
quote!(Some(#name))
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
let size = if let Some(s) = self.size {
|
||||
quote!(Some(#s))
|
||||
} else {
|
||||
@@ -284,6 +291,7 @@ impl ToTokens for StructureStruct {
|
||||
biomes: #biomes,
|
||||
step: #step,
|
||||
start_pool: #start_pool,
|
||||
start_jigsaw_name: #start_jigsaw_name,
|
||||
size: #size,
|
||||
terrain_adaptation: #terrain_adaptation,
|
||||
start_height: #start_height,
|
||||
@@ -604,6 +612,7 @@ pub fn build() -> TokenStream {
|
||||
pub biomes: &'static str,
|
||||
pub step: GenerationStep,
|
||||
pub start_pool: Option<&'static str>,
|
||||
pub start_jigsaw_name: Option<&'static str>,
|
||||
pub size: Option<i32>,
|
||||
pub terrain_adaptation: TerrainAdaptation,
|
||||
pub start_height: Option<i16>,
|
||||
|
||||
@@ -131,6 +131,7 @@ pub struct Structure {
|
||||
pub biomes: &'static str,
|
||||
pub step: GenerationStep,
|
||||
pub start_pool: Option<&'static str>,
|
||||
pub start_jigsaw_name: Option<&'static str>,
|
||||
pub size: Option<i32>,
|
||||
pub terrain_adaptation: TerrainAdaptation,
|
||||
pub start_height: Option<i16>,
|
||||
@@ -175,6 +176,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ancient_city",
|
||||
step: GenerationStep::UndergroundDecoration,
|
||||
start_pool: Some("minecraft:ancient_city/city_center"),
|
||||
start_jigsaw_name: Some("minecraft:city_anchor"),
|
||||
size: Some(7i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardBox,
|
||||
start_height: Some(-27i16),
|
||||
@@ -189,6 +191,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/bastion_remnant",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:bastion/starts"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(6i32),
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: Some(33i16),
|
||||
@@ -203,6 +206,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/buried_treasure",
|
||||
step: GenerationStep::UndergroundStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -217,6 +221,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/desert_pyramid",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -231,6 +236,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/end_city",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -245,6 +251,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/nether_fortress",
|
||||
step: GenerationStep::UndergroundDecoration,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -259,6 +266,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/igloo",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -273,6 +281,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/jungle_temple",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -287,6 +296,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/woodland_mansion",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -301,6 +311,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/mineshaft",
|
||||
step: GenerationStep::UndergroundStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -315,6 +326,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/mineshaft_mesa",
|
||||
step: GenerationStep::UndergroundStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -329,6 +341,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ocean_monument",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -343,6 +356,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/nether_fossil",
|
||||
step: GenerationStep::UndergroundDecoration,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: None,
|
||||
@@ -357,6 +371,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ocean_ruin_cold",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -371,6 +386,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ocean_ruin_warm",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -385,6 +401,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/pillager_outpost",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:pillager_outpost/base_plates"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(7i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: Some(0i16),
|
||||
@@ -399,6 +416,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_standard",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -413,6 +431,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_desert",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -427,6 +446,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_jungle",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -441,6 +461,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_mountain",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -455,6 +476,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_nether",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -469,6 +491,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_ocean",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -483,6 +506,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/ruined_portal_swamp",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -497,6 +521,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/shipwreck",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -511,6 +536,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/shipwreck_beached",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -525,6 +551,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/stronghold",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::Bury,
|
||||
start_height: None,
|
||||
@@ -539,6 +566,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/swamp_hut",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: None,
|
||||
start_jigsaw_name: None,
|
||||
size: None,
|
||||
terrain_adaptation: TerrainAdaptation::None,
|
||||
start_height: None,
|
||||
@@ -553,6 +581,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/trail_ruins",
|
||||
step: GenerationStep::UndergroundStructures,
|
||||
start_pool: Some("minecraft:trail_ruins/tower"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(7i32),
|
||||
terrain_adaptation: TerrainAdaptation::Bury,
|
||||
start_height: Some(-15i16),
|
||||
@@ -567,6 +596,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/trial_chambers",
|
||||
step: GenerationStep::UndergroundStructures,
|
||||
start_pool: Some("minecraft:trial_chambers/chamber/end"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(20i32),
|
||||
terrain_adaptation: TerrainAdaptation::Encapsulate,
|
||||
start_height: Some(-40i16),
|
||||
@@ -581,6 +611,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/village_desert",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:village/desert/town_centers"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(6i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: Some(0i16),
|
||||
@@ -595,6 +626,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/village_plains",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:village/plains/town_centers"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(6i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: Some(0i16),
|
||||
@@ -609,6 +641,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/village_savanna",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:village/savanna/town_centers"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(6i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: Some(0i16),
|
||||
@@ -623,6 +656,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/village_snowy",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:village/snowy/town_centers"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(6i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: Some(0i16),
|
||||
@@ -637,6 +671,7 @@ impl Structure {
|
||||
biomes: "#minecraft:has_structure/village_taiga",
|
||||
step: GenerationStep::SurfaceStructures,
|
||||
start_pool: Some("minecraft:village/taiga/town_centers"),
|
||||
start_jigsaw_name: None,
|
||||
size: Some(6i32),
|
||||
terrain_adaptation: TerrainAdaptation::BeardThin,
|
||||
start_height: Some(0i16),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"}]}
|
||||
@@ -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"}]}
|
||||
@@ -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"}]}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -12,6 +12,12 @@ fn main() {
|
||||
let mut pool_code = String::from(
|
||||
"pub fn get_pool_elements(pool_id: &str) -> Option<&'static [&'static str]> {\n match pool_id {\n",
|
||||
);
|
||||
let mut template_pool_json_code = String::from(
|
||||
"pub fn get_template_pool_json(path: &str) -> Option<&'static str> {\n match path {\n",
|
||||
);
|
||||
let mut processor_list_json_code = String::from(
|
||||
"pub fn get_processor_list_json(path: &str) -> Option<&'static str> {\n match path {\n",
|
||||
);
|
||||
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let assets_dir = Path::new(&manifest_dir).join("assets/structures");
|
||||
@@ -36,8 +42,29 @@ fn main() {
|
||||
pool_code.push_str(" _ => None,\n");
|
||||
pool_code.push_str(" }\n}\n");
|
||||
|
||||
fs::write(&dest_path, format!("{code}\n{pool_code}")).unwrap();
|
||||
let worldgen_dir = Path::new(&manifest_dir).join("assets/worldgen");
|
||||
process_json_dir(
|
||||
&worldgen_dir.join("template_pool"),
|
||||
"",
|
||||
&mut template_pool_json_code,
|
||||
);
|
||||
process_json_dir(
|
||||
&worldgen_dir.join("processor_list"),
|
||||
"",
|
||||
&mut processor_list_json_code,
|
||||
);
|
||||
template_pool_json_code.push_str(" _ => None,\n");
|
||||
template_pool_json_code.push_str(" }\n}\n");
|
||||
processor_list_json_code.push_str(" _ => None,\n");
|
||||
processor_list_json_code.push_str(" }\n}\n");
|
||||
|
||||
fs::write(
|
||||
&dest_path,
|
||||
format!("{code}\n{pool_code}\n{template_pool_json_code}\n{processor_list_json_code}"),
|
||||
)
|
||||
.unwrap();
|
||||
println!("cargo:rerun-if-changed=assets/structures");
|
||||
println!("cargo:rerun-if-changed=assets/worldgen");
|
||||
}
|
||||
|
||||
fn process_dir(
|
||||
@@ -76,3 +103,39 @@ fn process_dir(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_json_dir(dir: &Path, prefix: &str, code: &mut String) {
|
||||
if !dir.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut entries = fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(Result::unwrap)
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by_key(std::fs::DirEntry::file_name);
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().into_string().unwrap();
|
||||
if path.is_dir() {
|
||||
let new_prefix = if prefix.is_empty() {
|
||||
name
|
||||
} else {
|
||||
format!("{prefix}/{name}")
|
||||
};
|
||||
process_json_dir(&path, &new_prefix, code);
|
||||
} else if let Some(stem) = name.strip_suffix(".json") {
|
||||
let id = if prefix.is_empty() {
|
||||
stem.to_string()
|
||||
} else {
|
||||
format!("{prefix}/{stem}")
|
||||
};
|
||||
let abs_path = path.canonicalize().unwrap();
|
||||
code.push_str(&format!(
|
||||
" \"minecraft:{id}\" | \"{id}\" => Some(include_str!(r#\"{}\"#)),\n",
|
||||
abs_path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use pumpkin_data::Block;
|
||||
use pumpkin_data::tag::Block::MINECRAFT_SCULK_REPLACEABLE_WORLD_GEN;
|
||||
use pumpkin_data::{
|
||||
Block, BlockState,
|
||||
block_properties::{BlockProperties, GlowLichenLikeProperties, SculkShriekerLikeProperties},
|
||||
};
|
||||
use pumpkin_util::{
|
||||
math::{int_provider::IntProvider, position::BlockPos, vector3::Vector3},
|
||||
random::{RandomGenerator, RandomImpl},
|
||||
@@ -69,8 +72,57 @@ impl SculkPatchFeature {
|
||||
GenerationCache::get_block_state(chunk, &below_candidate.0).to_state();
|
||||
|
||||
if state.is_air() && below_state.is_side_solid(pumpkin_data::BlockDirection::Up) {
|
||||
// TODO: set sculk shrieker with can_summon = true if possible
|
||||
chunk.set_block_state(&candidate.0, Block::SCULK_SHRIEKER.default_state);
|
||||
chunk.set_block_state(&candidate.0, ancient_city_shrieker_state());
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn generate_in_proto_chunk(
|
||||
&self,
|
||||
chunk: &mut crate::ProtoChunk,
|
||||
random: &mut RandomGenerator,
|
||||
pos: BlockPos,
|
||||
) -> bool {
|
||||
if !can_spread_from_proto_chunk(chunk, pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut spreader = SculkSpreader::new();
|
||||
let total_rounds = self.spread_rounds + self.growth_rounds;
|
||||
for round in 0..total_rounds {
|
||||
for _ in 0..self.charge_count {
|
||||
spreader.add_cursor(pos, self.amount_per_charge);
|
||||
}
|
||||
for _ in 0..self.spread_attempts {
|
||||
spreader.update_cursors_in_proto_chunk(chunk, random, round < self.spread_rounds);
|
||||
}
|
||||
spreader.clear();
|
||||
}
|
||||
|
||||
let below = pos.down();
|
||||
if random.next_f32() <= self.catalyst_chance
|
||||
&& proto_chunk_state(chunk, below).is_some_and(|state| state.to_state().is_solid())
|
||||
{
|
||||
set_proto_chunk_state(chunk, pos, Block::SCULK_CATALYST.default_state);
|
||||
}
|
||||
|
||||
for _ in 0..self.extra_rare_growths.get(random) {
|
||||
let candidate = pos.offset(Vector3::new(
|
||||
random.next_bounded_i32(5) - 2,
|
||||
0,
|
||||
random.next_bounded_i32(5) - 2,
|
||||
));
|
||||
let below = candidate.down();
|
||||
if proto_chunk_state(chunk, candidate).is_some_and(|state| state.to_state().is_air())
|
||||
&& proto_chunk_state(chunk, below).is_some_and(|state| {
|
||||
state
|
||||
.to_state()
|
||||
.is_side_solid(pumpkin_data::BlockDirection::Up)
|
||||
})
|
||||
{
|
||||
set_proto_chunk_state(chunk, candidate, ancient_city_shrieker_state());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,10 +168,52 @@ fn is_sculk_behaviour(block_id: u16) -> bool {
|
||||
|| block_id == Block::CALIBRATED_SCULK_SENSOR.id
|
||||
}
|
||||
|
||||
fn ancient_city_shrieker_state() -> &'static BlockState {
|
||||
let mut properties = SculkShriekerLikeProperties::default(&Block::SCULK_SHRIEKER);
|
||||
properties.r#can_summon = true;
|
||||
BlockState::from_id(properties.to_state_id(&Block::SCULK_SHRIEKER))
|
||||
}
|
||||
|
||||
fn is_sculk_replaceable(block_id: u16) -> bool {
|
||||
MINECRAFT_SCULK_REPLACEABLE_WORLD_GEN.1.contains(&block_id)
|
||||
}
|
||||
|
||||
/// Resolves the sculk-vein block state to place at a position so that it clings to a sturdy
|
||||
/// neighbour on `face` (the direction from the vein position toward that neighbour). Existing
|
||||
/// sculk-vein states are merged; replaceable/air/water blocks become a fresh vein.
|
||||
fn sculk_vein_state_with_face(
|
||||
existing: &'static BlockState,
|
||||
face: pumpkin_data::BlockDirection,
|
||||
) -> Option<&'static BlockState> {
|
||||
let existing_block_id = Block::get_raw_id_from_state_id(existing.id);
|
||||
let is_vein = existing_block_id == Block::SCULK_VEIN.id;
|
||||
if !(is_vein || is_sculk_replaceable(existing_block_id) || existing_block_id == Block::WATER.id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut properties = if is_vein {
|
||||
GlowLichenLikeProperties::from_state_id(existing.id, &Block::SCULK_VEIN)
|
||||
} else {
|
||||
let mut properties = GlowLichenLikeProperties::default(&Block::SCULK_VEIN);
|
||||
properties.r#waterlogged = existing_block_id == Block::WATER.id;
|
||||
properties
|
||||
};
|
||||
|
||||
match face {
|
||||
pumpkin_data::BlockDirection::Down => properties.r#down = true,
|
||||
pumpkin_data::BlockDirection::Up => properties.r#up = true,
|
||||
pumpkin_data::BlockDirection::North => properties.r#north = true,
|
||||
pumpkin_data::BlockDirection::South => properties.r#south = true,
|
||||
pumpkin_data::BlockDirection::West => properties.r#west = true,
|
||||
pumpkin_data::BlockDirection::East => properties.r#east = true,
|
||||
}
|
||||
|
||||
Some(BlockState::from_id(
|
||||
properties.to_state_id(&Block::SCULK_VEIN),
|
||||
))
|
||||
}
|
||||
|
||||
struct Cursor {
|
||||
pos: BlockPos,
|
||||
charge: i32,
|
||||
@@ -148,7 +242,7 @@ impl SculkSpreader {
|
||||
&mut self,
|
||||
chunk: &mut T,
|
||||
random: &mut RandomGenerator,
|
||||
_spread_veins: bool,
|
||||
spread_veins: bool,
|
||||
) {
|
||||
let mut next_cursors = Vec::new();
|
||||
for mut cursor in self.cursors.drain(..) {
|
||||
@@ -167,6 +261,49 @@ impl SculkSpreader {
|
||||
|
||||
if is_sculk_replaceable(target_block_id) {
|
||||
chunk.set_block_state(&target_pos.0, Block::SCULK.default_state);
|
||||
if spread_veins {
|
||||
grow_sculk_veins(chunk, target_pos);
|
||||
}
|
||||
cursor.pos = target_pos;
|
||||
cursor.charge -= 1;
|
||||
} else if target_block_id == Block::SCULK.id {
|
||||
cursor.pos = target_pos;
|
||||
cursor.charge -= 1;
|
||||
}
|
||||
|
||||
if cursor.charge > 0 {
|
||||
next_cursors.push(cursor);
|
||||
}
|
||||
}
|
||||
self.cursors = next_cursors;
|
||||
}
|
||||
|
||||
fn update_cursors_in_proto_chunk(
|
||||
&mut self,
|
||||
chunk: &mut crate::ProtoChunk,
|
||||
random: &mut RandomGenerator,
|
||||
spread_veins: bool,
|
||||
) {
|
||||
let mut next_cursors = Vec::new();
|
||||
for mut cursor in self.cursors.drain(..) {
|
||||
if cursor.charge <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let target_pos = cursor.pos.offset(Vector3::new(
|
||||
random.next_bounded_i32(3) - 1,
|
||||
random.next_bounded_i32(3) - 1,
|
||||
random.next_bounded_i32(3) - 1,
|
||||
));
|
||||
let Some(target_state) = proto_chunk_state(chunk, target_pos) else {
|
||||
continue;
|
||||
};
|
||||
let target_block_id = target_state.to_block_id();
|
||||
if is_sculk_replaceable(target_block_id) {
|
||||
set_proto_chunk_state(chunk, target_pos, Block::SCULK.default_state);
|
||||
if spread_veins {
|
||||
grow_sculk_veins_in_proto_chunk(chunk, target_pos);
|
||||
}
|
||||
cursor.pos = target_pos;
|
||||
cursor.charge -= 1;
|
||||
} else if target_block_id == Block::SCULK.id {
|
||||
@@ -181,3 +318,69 @@ impl SculkSpreader {
|
||||
self.cursors = next_cursors;
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_chunk_state(
|
||||
chunk: &crate::ProtoChunk,
|
||||
pos: BlockPos,
|
||||
) -> Option<crate::block::RawBlockState> {
|
||||
((pos.0.x >> 4) == chunk.x && (pos.0.z >> 4) == chunk.z).then(|| chunk.get_block_state(&pos.0))
|
||||
}
|
||||
|
||||
fn set_proto_chunk_state(
|
||||
chunk: &mut crate::ProtoChunk,
|
||||
pos: BlockPos,
|
||||
state: &'static pumpkin_data::BlockState,
|
||||
) {
|
||||
if (pos.0.x >> 4) == chunk.x && (pos.0.z >> 4) == chunk.z {
|
||||
chunk.set_block_state(pos.0.x, pos.0.y, pos.0.z, state);
|
||||
}
|
||||
}
|
||||
|
||||
fn grow_sculk_veins<T: GenerationCache>(chunk: &mut T, sculk_pos: BlockPos) {
|
||||
for dir in pumpkin_data::BlockDirection::all() {
|
||||
let vein_pos = sculk_pos.offset(dir.to_offset());
|
||||
let existing = GenerationCache::get_block_state(chunk, &vein_pos.0).to_state();
|
||||
if let Some(state) = sculk_vein_state_with_face(existing, dir.opposite()) {
|
||||
chunk.set_block_state(&vein_pos.0, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grow_sculk_veins_in_proto_chunk(chunk: &mut crate::ProtoChunk, sculk_pos: BlockPos) {
|
||||
for dir in pumpkin_data::BlockDirection::all() {
|
||||
let vein_pos = sculk_pos.offset(dir.to_offset());
|
||||
let Some(existing) = proto_chunk_state(chunk, vein_pos) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(state) = sculk_vein_state_with_face(existing.to_state(), dir.opposite()) {
|
||||
set_proto_chunk_state(chunk, vein_pos, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn can_spread_from_proto_chunk(chunk: &crate::ProtoChunk, pos: BlockPos) -> bool {
|
||||
let Some(state) = proto_chunk_state(chunk, pos) else {
|
||||
return false;
|
||||
};
|
||||
let block_id = state.to_block_id();
|
||||
if is_sculk_behaviour(block_id) {
|
||||
return true;
|
||||
}
|
||||
if !state.to_state().is_air() && block_id != Block::WATER.id {
|
||||
return false;
|
||||
}
|
||||
|
||||
[
|
||||
Vector3::new(1, 0, 0),
|
||||
Vector3::new(-1, 0, 0),
|
||||
Vector3::new(0, 1, 0),
|
||||
Vector3::new(0, -1, 0),
|
||||
Vector3::new(0, 0, 1),
|
||||
Vector3::new(0, 0, -1),
|
||||
]
|
||||
.into_iter()
|
||||
.any(|offset| {
|
||||
proto_chunk_state(chunk, pos.offset(offset))
|
||||
.is_some_and(|state| state.to_state().is_solid())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,6 +49,30 @@ pub enum Feature {
|
||||
}
|
||||
|
||||
impl PlacedFeature {
|
||||
pub fn generate_in_proto_chunk(
|
||||
&self,
|
||||
chunk: &mut crate::ProtoChunk,
|
||||
feature_name: pumpkin_data::placed_feature::PlacedFeature,
|
||||
random: &mut RandomGenerator,
|
||||
pos: BlockPos,
|
||||
) -> bool {
|
||||
let feature = match &self.feature {
|
||||
Feature::Named(name) => CONFIGURED_FEATURES
|
||||
.get(name)
|
||||
.expect("Name: {name:?} not found"),
|
||||
Feature::Inlined(feature) => feature,
|
||||
};
|
||||
match feature {
|
||||
ConfiguredFeature::SculkPatch(feature) => {
|
||||
feature.generate_in_proto_chunk(chunk, random, pos)
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Placed feature {feature_name:?} is not supported in a jigsaw pool");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn generate<T: GenerationCache>(
|
||||
&self,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -5,10 +5,11 @@ use crate::generation::structure::structures::{
|
||||
StructureGenerator, StructureGeneratorContext, StructurePieceBase, StructurePosition,
|
||||
};
|
||||
use crate::generation::structure::template::{
|
||||
BlockMirror, BlockRotation, BlockStateResolver, PaletteEntry, StructureTemplate,
|
||||
BlockMirror, BlockRotation, PaletteEntry, StructureTemplate,
|
||||
};
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::random::RandomImpl;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
@@ -26,9 +27,193 @@ pub struct TemplatePool {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PoolElement {
|
||||
pub template: &'static str,
|
||||
pub weight: u32,
|
||||
pub projection: JigsawProjection,
|
||||
pub kind: PoolElementKind,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PoolElementKind {
|
||||
Empty,
|
||||
Single {
|
||||
template: String,
|
||||
processors: ProcessorListRef,
|
||||
},
|
||||
List(Vec<PoolElementKind>),
|
||||
Feature(pumpkin_data::placed_feature::PlacedFeature),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub enum ProcessorListRef {
|
||||
Named(String),
|
||||
#[default]
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawTemplatePool {
|
||||
fallback: String,
|
||||
elements: Vec<RawWeightedPoolElement>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawWeightedPoolElement {
|
||||
element: RawPoolElement,
|
||||
weight: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "element_type")]
|
||||
enum RawPoolElement {
|
||||
#[serde(rename = "minecraft:empty_pool_element")]
|
||||
Empty,
|
||||
#[serde(rename = "minecraft:single_pool_element")]
|
||||
Single {
|
||||
location: String,
|
||||
processors: RawProcessorList,
|
||||
projection: RawProjection,
|
||||
},
|
||||
#[serde(rename = "minecraft:list_pool_element")]
|
||||
List {
|
||||
elements: Vec<RawPoolElement>,
|
||||
projection: RawProjection,
|
||||
},
|
||||
#[serde(rename = "minecraft:feature_pool_element")]
|
||||
Feature {
|
||||
feature: String,
|
||||
projection: RawProjection,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawProcessorList {
|
||||
Named(String),
|
||||
Inline { processors: Vec<serde_json::Value> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum RawProjection {
|
||||
Rigid,
|
||||
TerrainMatching,
|
||||
}
|
||||
|
||||
impl From<RawProjection> for JigsawProjection {
|
||||
fn from(value: RawProjection) -> Self {
|
||||
match value {
|
||||
RawProjection::Rigid => Self::Rigid,
|
||||
RawProjection::TerrainMatching => Self::TerrainMatching,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawPoolElement {
|
||||
fn into_element(self) -> Option<(PoolElementKind, JigsawProjection)> {
|
||||
match self {
|
||||
Self::Empty => Some((PoolElementKind::Empty, JigsawProjection::Rigid)),
|
||||
Self::Single {
|
||||
location,
|
||||
processors,
|
||||
projection,
|
||||
} => {
|
||||
let processors = match processors {
|
||||
RawProcessorList::Named(name) => ProcessorListRef::Named(name),
|
||||
RawProcessorList::Inline { processors } => {
|
||||
debug_assert!(processors.is_empty());
|
||||
ProcessorListRef::Empty
|
||||
}
|
||||
};
|
||||
Some((
|
||||
PoolElementKind::Single {
|
||||
template: location,
|
||||
processors,
|
||||
},
|
||||
projection.into(),
|
||||
))
|
||||
}
|
||||
Self::List {
|
||||
elements,
|
||||
projection,
|
||||
} => {
|
||||
let projection = projection.into();
|
||||
let elements = elements
|
||||
.into_iter()
|
||||
.filter_map(|element| element.into_element().map(|(kind, _)| kind))
|
||||
.collect();
|
||||
Some((PoolElementKind::List(elements), projection))
|
||||
}
|
||||
Self::Feature {
|
||||
feature,
|
||||
projection,
|
||||
} => {
|
||||
let feature = feature.strip_prefix("minecraft:").unwrap_or(&feature);
|
||||
pumpkin_data::placed_feature::PlacedFeature::from_name(feature)
|
||||
.map(|feature| (PoolElementKind::Feature(feature), projection.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PoolElement {
|
||||
#[must_use]
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
matches!(self.kind, PoolElementKind::Empty)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn first_template(&self) -> Option<Arc<StructureTemplate>> {
|
||||
fn find(kind: &PoolElementKind) -> Option<Arc<StructureTemplate>> {
|
||||
match kind {
|
||||
PoolElementKind::Single { template, .. } => {
|
||||
crate::generation::structure::template::get_template(template)
|
||||
}
|
||||
PoolElementKind::List(elements) => elements.iter().find_map(find),
|
||||
PoolElementKind::Empty | PoolElementKind::Feature(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
find(&self.kind)
|
||||
}
|
||||
|
||||
pub fn for_each_template(
|
||||
&self,
|
||||
mut consumer: impl FnMut(&str, &ProcessorListRef, Arc<StructureTemplate>),
|
||||
) {
|
||||
fn visit(
|
||||
kind: &PoolElementKind,
|
||||
consumer: &mut impl FnMut(&str, &ProcessorListRef, Arc<StructureTemplate>),
|
||||
) {
|
||||
match kind {
|
||||
PoolElementKind::Single {
|
||||
template,
|
||||
processors,
|
||||
} => {
|
||||
if let Some(structure_template) =
|
||||
crate::generation::structure::template::get_template(template)
|
||||
{
|
||||
consumer(template, processors, structure_template);
|
||||
}
|
||||
}
|
||||
PoolElementKind::List(elements) => {
|
||||
for element in elements {
|
||||
visit(element, consumer);
|
||||
}
|
||||
}
|
||||
PoolElementKind::Empty | PoolElementKind::Feature(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
visit(&self.kind, &mut consumer);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn feature(&self) -> Option<pumpkin_data::placed_feature::PlacedFeature> {
|
||||
match self.kind {
|
||||
PoolElementKind::Feature(feature) => Some(feature),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplatePool {
|
||||
@@ -53,27 +238,91 @@ impl TemplatePool {
|
||||
/// Discovers a pool from the filesystem/embedded assets.
|
||||
#[must_use]
|
||||
pub fn discover(id: &str) -> Option<Self> {
|
||||
let elements = crate::generation::structure::template::get_pool_elements(id)?;
|
||||
static CACHE: std::sync::LazyLock<dashmap::DashMap<String, TemplatePool>> =
|
||||
std::sync::LazyLock::new(dashmap::DashMap::new);
|
||||
|
||||
// Heuristic: roads and streets are usually TerrainMatching
|
||||
let projection = if id.contains("streets") {
|
||||
JigsawProjection::TerrainMatching
|
||||
} else {
|
||||
JigsawProjection::Rigid
|
||||
};
|
||||
if let Some(pool) = CACHE.get(id) {
|
||||
return Some(pool.clone());
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
id: id.to_string(),
|
||||
fallback: "minecraft:empty".to_string(),
|
||||
elements: elements
|
||||
.iter()
|
||||
.map(|e| PoolElement {
|
||||
template: e,
|
||||
weight: 1,
|
||||
projection,
|
||||
let pool = if id == "minecraft:empty" || id == "empty" {
|
||||
Self {
|
||||
id: "minecraft:empty".to_string(),
|
||||
fallback: "minecraft:empty".to_string(),
|
||||
elements: Vec::new(),
|
||||
}
|
||||
} else if let Some(json) =
|
||||
crate::generation::structure::template::get_template_pool_json(id)
|
||||
{
|
||||
let raw: RawTemplatePool = match serde_json::from_str(json) {
|
||||
Ok(pool) => pool,
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to parse template pool {id}: {error}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let elements = raw
|
||||
.elements
|
||||
.into_iter()
|
||||
.filter_map(|weighted| {
|
||||
weighted
|
||||
.element
|
||||
.into_element()
|
||||
.map(|(kind, projection)| PoolElement {
|
||||
weight: weighted.weight,
|
||||
projection,
|
||||
kind,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
fallback: raw.fallback,
|
||||
elements,
|
||||
}
|
||||
} else {
|
||||
let elements = crate::generation::structure::template::get_pool_elements(id)?;
|
||||
let projection = if id.contains("streets") {
|
||||
JigsawProjection::TerrainMatching
|
||||
} else {
|
||||
JigsawProjection::Rigid
|
||||
};
|
||||
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
fallback: "minecraft:empty".to_string(),
|
||||
elements: elements
|
||||
.iter()
|
||||
.map(|e| PoolElement {
|
||||
weight: 1,
|
||||
projection,
|
||||
kind: PoolElementKind::Single {
|
||||
template: (*e).to_string(),
|
||||
processors: ProcessorListRef::Empty,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
};
|
||||
CACHE.insert(id.to_owned(), pool.clone());
|
||||
Some(pool)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_shuffled_elements(
|
||||
&self,
|
||||
random: &mut pumpkin_util::random::RandomGenerator,
|
||||
) -> Vec<PoolElement> {
|
||||
let mut elements = self
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|element| std::iter::repeat_n(element.clone(), element.weight as usize))
|
||||
.collect::<Vec<_>>();
|
||||
for index in (1..elements.len()).rev() {
|
||||
let other = random.next_bounded_i32(index as i32 + 1) as usize;
|
||||
elements.swap(index, other);
|
||||
}
|
||||
elements
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +438,7 @@ pub struct JigsawJunction {
|
||||
|
||||
pub struct PoolElementStructurePiece {
|
||||
pub piece: crate::generation::structure::structures::StructurePiece,
|
||||
pub template: Arc<StructureTemplate>,
|
||||
pub element: PoolElement,
|
||||
pub pos: BlockPos,
|
||||
pub rotation: BlockRotation,
|
||||
pub mirror: BlockMirror,
|
||||
@@ -218,55 +467,39 @@ impl StructurePieceBase for PoolElementStructurePiece {
|
||||
&mut self,
|
||||
chunk: &mut crate::ProtoChunk,
|
||||
_block_registry: &dyn crate::world::WorldPortalExt,
|
||||
_random: &mut pumpkin_util::random::RandomGenerator,
|
||||
random: &mut pumpkin_util::random::RandomGenerator,
|
||||
_seed: i64,
|
||||
chunk_box: &pumpkin_util::math::block_box::BlockBox,
|
||||
) {
|
||||
let origin =
|
||||
pumpkin_util::math::vector3::Vector3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z);
|
||||
|
||||
let processors: Vec<Box<dyn crate::generation::structure::template::StructureProcessor>> =
|
||||
vec![Box::new(
|
||||
crate::generation::structure::template::processor::GravityProcessor {
|
||||
heightmap: pumpkin_util::HeightMap::WorldSurfaceWg,
|
||||
offset: -1,
|
||||
},
|
||||
)];
|
||||
self.element
|
||||
.for_each_template(|_name, processor_list, template| {
|
||||
let processors = match processor_list {
|
||||
ProcessorListRef::Named(name) => {
|
||||
crate::generation::structure::template::processor::load_processor_list(name)
|
||||
}
|
||||
ProcessorListRef::Empty => Arc::from([]),
|
||||
};
|
||||
crate::generation::structure::template::place_template(
|
||||
chunk,
|
||||
&template,
|
||||
origin,
|
||||
(0, 0),
|
||||
self.rotation,
|
||||
false,
|
||||
self.liquid_settings == LiquidSettings::ApplyWaterlog,
|
||||
processors.as_ref(),
|
||||
Some(chunk_box),
|
||||
);
|
||||
});
|
||||
|
||||
crate::generation::structure::template::place_template(
|
||||
chunk,
|
||||
&self.template,
|
||||
origin,
|
||||
(0, 0),
|
||||
self.rotation,
|
||||
false,
|
||||
&processors,
|
||||
Some(chunk_box),
|
||||
);
|
||||
|
||||
// Post-process: replace jigsaw blocks with their final_state
|
||||
for jigsaw in &self.jigsaw_blocks {
|
||||
let wx = jigsaw.pos.0.x;
|
||||
let wy = jigsaw.pos.0.y;
|
||||
let wz = jigsaw.pos.0.z;
|
||||
if wx < chunk_box.min.x
|
||||
|| wx > chunk_box.max.x
|
||||
|| wy < chunk_box.min.y
|
||||
|| wy > chunk_box.max.y
|
||||
|| wz < chunk_box.min.z
|
||||
|| wz > chunk_box.max.z
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if jigsaw.final_state == "minecraft:air" || jigsaw.final_state == "air" {
|
||||
chunk.set_block_state(wx, wy, wz, pumpkin_data::Block::AIR.default_state);
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry = PaletteEntry::from_string(&jigsaw.final_state);
|
||||
if let Some(state) = BlockStateResolver::resolve(&entry, self.rotation, self.mirror) {
|
||||
chunk.set_block_state(wx, wy, wz, state);
|
||||
}
|
||||
if let Some(feature) = self.element.feature()
|
||||
&& let Some(placed_feature) =
|
||||
crate::generation::feature::placed_features::PLACED_FEATURES.get(&feature)
|
||||
{
|
||||
placed_feature.generate_in_proto_chunk(chunk, feature, random, self.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,13 +596,123 @@ impl StructureGenerator for JigsawGenerator {
|
||||
start_pos,
|
||||
self.use_expansion_hack,
|
||||
project_start_to_heightmap,
|
||||
MaxDistance {
|
||||
horizontal: max_distance,
|
||||
vertical: max_distance,
|
||||
},
|
||||
MaxDistance::new(max_distance),
|
||||
dimension_padding,
|
||||
liquid_settings,
|
||||
&PoolAliasLookup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ancient_city_pools_match_vanilla_weights() {
|
||||
let expected = [
|
||||
("minecraft:ancient_city/city_center", 3, 3),
|
||||
("minecraft:ancient_city/sculk", 2, 7),
|
||||
("minecraft:ancient_city/structures", 20, 46),
|
||||
("minecraft:ancient_city/walls", 16, 27),
|
||||
("minecraft:ancient_city/city/entrance", 6, 6),
|
||||
("minecraft:ancient_city/city_center/walls", 10, 10),
|
||||
("minecraft:ancient_city/walls/no_corners", 8, 8),
|
||||
];
|
||||
|
||||
for (id, element_count, total_weight) in expected {
|
||||
let pool = TemplatePool::discover(id).unwrap_or_else(|| panic!("missing pool {id}"));
|
||||
assert_eq!(pool.elements.len(), element_count, "{id}");
|
||||
assert_eq!(
|
||||
pool.elements
|
||||
.iter()
|
||||
.map(|element| element.weight)
|
||||
.sum::<u32>(),
|
||||
total_weight,
|
||||
"{id}"
|
||||
);
|
||||
assert_eq!(pool.fallback, "minecraft:empty", "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ancient_city_start_templates_and_anchor_exist() {
|
||||
let pool = TemplatePool::discover("minecraft:ancient_city/city_center").unwrap();
|
||||
for element in pool.elements {
|
||||
let template = element.first_template().expect("missing start template");
|
||||
assert!(
|
||||
template.blocks.iter().any(|block| {
|
||||
JigsawBlock::from_template_block(block, &template.palette[block.state as usize])
|
||||
.is_some_and(|jigsaw| jigsaw.name == "minecraft:city_anchor")
|
||||
}),
|
||||
"start template has no city_anchor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ancient_city_pool_templates_are_embedded() {
|
||||
fn check(kind: &PoolElementKind) {
|
||||
match kind {
|
||||
PoolElementKind::Single { template, .. } => {
|
||||
// This entry exists in vanilla's pool data but has no corresponding
|
||||
// template in the vanilla server jar.
|
||||
if template == "minecraft:ancient_city/walls/intact_horizontal_wall_stairs_5" {
|
||||
assert!(
|
||||
crate::generation::structure::template::get_template(template)
|
||||
.is_none()
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
crate::generation::structure::template::get_template(template)
|
||||
.is_some(),
|
||||
"missing template {template}"
|
||||
);
|
||||
}
|
||||
}
|
||||
PoolElementKind::List(elements) => elements.iter().for_each(check),
|
||||
PoolElementKind::Empty | PoolElementKind::Feature(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
for id in [
|
||||
"minecraft:ancient_city/city_center",
|
||||
"minecraft:ancient_city/structures",
|
||||
"minecraft:ancient_city/walls",
|
||||
"minecraft:ancient_city/city/entrance",
|
||||
"minecraft:ancient_city/city_center/walls",
|
||||
"minecraft:ancient_city/walls/no_corners",
|
||||
] {
|
||||
for element in TemplatePool::discover(id).unwrap().elements {
|
||||
check(&element.kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ancient_city_builds_a_multi_piece_graph() {
|
||||
let generator = JigsawGenerator::new("minecraft:ancient_city/city_center", 7)
|
||||
.with_start_jigsaw("minecraft:city_anchor");
|
||||
let context = StructureGeneratorContext {
|
||||
seed: 0,
|
||||
chunk_x: 0,
|
||||
chunk_z: 0,
|
||||
random: super::super::create_chunk_random(0, 0, 0),
|
||||
sea_level: 63,
|
||||
min_y: -64,
|
||||
height_sampler: None,
|
||||
structure_key: Some(pumpkin_data::structures::StructureKeys::AncientCity),
|
||||
};
|
||||
|
||||
let position = generator
|
||||
.get_structure_position(context)
|
||||
.expect("ancient city graph should generate");
|
||||
let collector = position.collector.lock().unwrap();
|
||||
assert!(
|
||||
collector.pieces.len() > 10,
|
||||
"ancient city generated only {} pieces",
|
||||
collector.pieces.len()
|
||||
);
|
||||
assert_eq!(position.start_pos.0.y, -27);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use std::collections::BinaryHeap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::jigsaw::{
|
||||
JigsawBlock, JigsawJointType, JigsawJunction, JigsawProjection, PoolElementStructurePiece,
|
||||
TemplatePool,
|
||||
JigsawBlock, JigsawJointType, JigsawJunction, JigsawProjection, PoolElement, PoolElementKind,
|
||||
PoolElementStructurePiece, TemplatePool,
|
||||
};
|
||||
|
||||
pub struct JigsawPlacement;
|
||||
@@ -86,8 +86,8 @@ impl JigsawPlacement {
|
||||
|
||||
let actual_start_pool_id = pool_alias_lookup.lookup(start_pool_id);
|
||||
let pool = TemplatePool::discover(actual_start_pool_id)?;
|
||||
let element = pool.get_random_element(&mut context.random);
|
||||
let template = get_template(element.template)?;
|
||||
let element = pool.get_random_element(&mut context.random).clone();
|
||||
let template = element.first_template()?;
|
||||
|
||||
let rotation = Rotation::from_index(context.random.next_bounded_i32(4) as u8);
|
||||
|
||||
@@ -183,7 +183,7 @@ impl JigsawPlacement {
|
||||
box_,
|
||||
0,
|
||||
),
|
||||
template: Arc::clone(&template),
|
||||
element: element.clone(),
|
||||
pos: BlockPos::new(adjusted_position.0.x, bottom_y, adjusted_position.0.z),
|
||||
rotation,
|
||||
mirror: Mirror::None,
|
||||
@@ -243,34 +243,25 @@ impl JigsawPlacement {
|
||||
|
||||
let mut target_elements = Vec::new();
|
||||
if depth < max_depth {
|
||||
let mut main_elements = target_pool.elements.clone();
|
||||
for i in (1..main_elements.len()).rev() {
|
||||
let j = context.random.next_bounded_i32(i as i32 + 1) as usize;
|
||||
main_elements.swap(i, j);
|
||||
}
|
||||
target_elements.extend(main_elements);
|
||||
target_elements
|
||||
.extend(target_pool.get_shuffled_elements(&mut context.random));
|
||||
}
|
||||
|
||||
let fallback_pool_id = pool_alias_lookup.lookup(&target_pool.fallback);
|
||||
if let Some(fallback_pool) = TemplatePool::discover(fallback_pool_id) {
|
||||
let mut fallback_elements = fallback_pool.elements.clone();
|
||||
for i in (1..fallback_elements.len()).rev() {
|
||||
let j = context.random.next_bounded_i32(i as i32 + 1) as usize;
|
||||
fallback_elements.swap(i, j);
|
||||
}
|
||||
target_elements.extend(fallback_elements);
|
||||
target_elements
|
||||
.extend(fallback_pool.get_shuffled_elements(&mut context.random));
|
||||
}
|
||||
|
||||
for element in target_elements {
|
||||
if element.template == "minecraft:empty" {
|
||||
if element.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let target_template_arc = match get_template(element.template) {
|
||||
Some(t) => t,
|
||||
let target_size = match get_element_size(&element) {
|
||||
Some(size) => size,
|
||||
None => continue,
|
||||
};
|
||||
let target_template = target_template_arc.as_ref();
|
||||
let target_projection = element.projection;
|
||||
let target_rigid = target_projection == JigsawProjection::Rigid;
|
||||
|
||||
@@ -287,7 +278,7 @@ impl JigsawPlacement {
|
||||
}
|
||||
|
||||
for target_rotation in rotations {
|
||||
let target_jigsaws = get_jigsaw_blocks(target_template);
|
||||
let target_jigsaws = get_element_jigsaw_blocks(&element);
|
||||
|
||||
let mut target_jigsaws_shuffled = target_jigsaws.clone();
|
||||
for i in (1..target_jigsaws_shuffled.len()).rev() {
|
||||
@@ -310,8 +301,8 @@ impl JigsawPlacement {
|
||||
|
||||
let source_jigsaw_local_y =
|
||||
source_jigsaw_pos.0.y - source_box.min.y;
|
||||
let target_jigsaw_local_pos = target_rotation
|
||||
.transform_pos(target_jigsaw.pos.0, target_template.size);
|
||||
let target_jigsaw_local_pos =
|
||||
target_rotation.transform_pos(target_jigsaw.pos.0, target_size);
|
||||
let target_jigsaw_local_y = target_jigsaw_local_pos.y;
|
||||
|
||||
let delta_y = source_jigsaw_local_y - target_jigsaw_local_y
|
||||
@@ -354,7 +345,7 @@ impl JigsawPlacement {
|
||||
target_pos.0.y += y_offset;
|
||||
|
||||
let rotated_target_size =
|
||||
target_rotation.transform_size(target_template.size);
|
||||
target_rotation.transform_size(target_size);
|
||||
let mut target_box = BlockBox::new(
|
||||
target_pos.0.x,
|
||||
target_pos.0.y,
|
||||
@@ -371,13 +362,13 @@ impl JigsawPlacement {
|
||||
for tj in &target_jigsaws {
|
||||
let tj_facing =
|
||||
rotate_direction(tj.facing, target_rotation);
|
||||
let rotated_tj_pos = target_rotation
|
||||
.transform_pos(tj.pos.0, target_template.size);
|
||||
let rotated_tj_pos =
|
||||
target_rotation.transform_pos(tj.pos.0, target_size);
|
||||
let rotated_tj_target_pos =
|
||||
rotated_tj_pos.add(&tj_facing.to_vector());
|
||||
|
||||
let rotated_size =
|
||||
target_rotation.transform_size(target_template.size);
|
||||
target_rotation.transform_size(target_size);
|
||||
let hack_box = BlockBox::new(
|
||||
0,
|
||||
0,
|
||||
@@ -423,23 +414,17 @@ impl JigsawPlacement {
|
||||
|
||||
if !intersects_any(&pieces, &target_box) {
|
||||
let mut child_jigsaw_blocks = Vec::new();
|
||||
for block in &target_template.blocks {
|
||||
if let Some(mut cj) = JigsawBlock::from_template_block(
|
||||
block,
|
||||
&target_template.palette[block.state as usize],
|
||||
) {
|
||||
let rotated_pos = target_rotation
|
||||
.transform_pos(cj.pos.0, target_template.size);
|
||||
cj.pos = BlockPos(rotated_pos).add(
|
||||
target_pos.0.x,
|
||||
target_pos.0.y,
|
||||
target_pos.0.z,
|
||||
);
|
||||
cj.facing =
|
||||
rotate_direction(cj.facing, target_rotation);
|
||||
cj.up = rotate_direction(cj.up, target_rotation);
|
||||
child_jigsaw_blocks.push(cj);
|
||||
}
|
||||
for mut cj in get_element_jigsaw_blocks(&element) {
|
||||
let rotated_pos =
|
||||
target_rotation.transform_pos(cj.pos.0, target_size);
|
||||
cj.pos = BlockPos(rotated_pos).add(
|
||||
target_pos.0.x,
|
||||
target_pos.0.y,
|
||||
target_pos.0.z,
|
||||
);
|
||||
cj.facing = rotate_direction(cj.facing, target_rotation);
|
||||
cj.up = rotate_direction(cj.up, target_rotation);
|
||||
child_jigsaw_blocks.push(cj);
|
||||
}
|
||||
|
||||
let source_ground_level_delta =
|
||||
@@ -456,7 +441,7 @@ impl JigsawPlacement {
|
||||
target_box,
|
||||
depth as u32 + 1,
|
||||
),
|
||||
template: target_template_arc.clone(),
|
||||
element: element.clone(),
|
||||
pos: target_pos,
|
||||
rotation: target_rotation,
|
||||
mirror: Mirror::None,
|
||||
@@ -552,11 +537,11 @@ fn get_pool_max_y_size(pool_id: &str) -> i32 {
|
||||
|
||||
let mut max_y = 0;
|
||||
for element in &pool.elements {
|
||||
if element.template == "minecraft:empty" {
|
||||
if element.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(template) = get_template(element.template) {
|
||||
max_y = max_y.max(template.size.y);
|
||||
if let Some(size) = get_element_size(element) {
|
||||
max_y = max_y.max(size.y);
|
||||
}
|
||||
}
|
||||
max_y
|
||||
@@ -629,8 +614,60 @@ fn get_jigsaw_blocks(template: &StructureTemplate) -> Vec<JigsawBlock> {
|
||||
jigsaws
|
||||
}
|
||||
|
||||
fn get_element_size(element: &PoolElement) -> Option<pumpkin_util::math::vector3::Vector3<i32>> {
|
||||
fn size_for_kind(kind: &PoolElementKind) -> Option<pumpkin_util::math::vector3::Vector3<i32>> {
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
|
||||
match kind {
|
||||
PoolElementKind::Empty => None,
|
||||
PoolElementKind::Feature(_) => Some(Vector3::new(1, 1, 1)),
|
||||
PoolElementKind::Single { template, .. } => get_template(template).map(|t| t.size),
|
||||
PoolElementKind::List(elements) => {
|
||||
let mut result = Vector3::new(0, 0, 0);
|
||||
let mut found = false;
|
||||
for size in elements.iter().filter_map(size_for_kind) {
|
||||
result.x = result.x.max(size.x);
|
||||
result.y = result.y.max(size.y);
|
||||
result.z = result.z.max(size.z);
|
||||
found = true;
|
||||
}
|
||||
found.then_some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_for_kind(&element.kind)
|
||||
}
|
||||
|
||||
fn get_element_jigsaw_blocks(element: &PoolElement) -> Vec<JigsawBlock> {
|
||||
fn jigsaws_for_kind(kind: &PoolElementKind) -> Vec<JigsawBlock> {
|
||||
match kind {
|
||||
PoolElementKind::Single { template, .. } => get_template(template)
|
||||
.map_or_else(Vec::new, |template| get_jigsaw_blocks(&template)),
|
||||
PoolElementKind::List(elements) => {
|
||||
elements.first().map_or_else(Vec::new, jigsaws_for_kind)
|
||||
}
|
||||
PoolElementKind::Feature(_) => vec![JigsawBlock {
|
||||
pos: BlockPos::new(0, 0, 0),
|
||||
name: "minecraft:bottom".to_string(),
|
||||
target: "minecraft:empty".to_string(),
|
||||
pool: "minecraft:empty".to_string(),
|
||||
final_state: "minecraft:air".to_string(),
|
||||
joint: JigsawJointType::Rollable,
|
||||
facing: pumpkin_util::BlockDirection::Down,
|
||||
up: pumpkin_util::BlockDirection::South,
|
||||
selection_priority: 0,
|
||||
placement_priority: 0,
|
||||
}],
|
||||
PoolElementKind::Empty => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
jigsaws_for_kind(&element.kind)
|
||||
}
|
||||
|
||||
fn can_attach(source: &JigsawBlock, target: &JigsawBlock, target_rotation: Rotation) -> bool {
|
||||
if source.target != target.name || target.target != source.name {
|
||||
if source.target != target.name {
|
||||
return false;
|
||||
}
|
||||
let rotated_target_facing = rotate_direction(target.facing, target_rotation);
|
||||
@@ -638,11 +675,7 @@ fn can_attach(source: &JigsawBlock, target: &JigsawBlock, target_rotation: Rotat
|
||||
return false;
|
||||
}
|
||||
|
||||
// Joint alignment for vertical connections
|
||||
if (source.joint == JigsawJointType::Aligned || target.joint == JigsawJointType::Aligned)
|
||||
&& (source.facing == pumpkin_util::BlockDirection::Up
|
||||
|| source.facing == pumpkin_util::BlockDirection::Down)
|
||||
{
|
||||
if source.joint == JigsawJointType::Aligned {
|
||||
let rotated_target_up = rotate_direction(target.up, target_rotation);
|
||||
return source.up == rotated_target_up;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use pumpkin_util::HeightMap;
|
||||
use pumpkin_util::{
|
||||
BlockDirection,
|
||||
math::{block_box::BlockBox, position::BlockPos, vector3::Vector3},
|
||||
random::{RandomGenerator, RandomImpl, get_carver_seed, xoroshiro128::Xoroshiro},
|
||||
random::{RandomGenerator, RandomImpl, legacy_rand::LegacyRand},
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
@@ -685,8 +685,13 @@ pub struct StructureGeneratorContext<'a> {
|
||||
|
||||
#[must_use]
|
||||
pub fn create_chunk_random(seed: i64, chunk_x: i32, chunk_z: i32) -> RandomGenerator {
|
||||
let carver_seed = get_carver_seed(seed as u64, chunk_x, chunk_z);
|
||||
RandomGenerator::Xoroshiro(Xoroshiro::from_seed(carver_seed))
|
||||
let mut seeder = LegacyRand::from_seed(seed as u64);
|
||||
let x_multiplier = seeder.next_i64();
|
||||
let z_multiplier = seeder.next_i64();
|
||||
let structure_seed = (i64::from(chunk_x).wrapping_mul(x_multiplier))
|
||||
^ (i64::from(chunk_z).wrapping_mul(z_multiplier))
|
||||
^ seed;
|
||||
RandomGenerator::Legacy(LegacyRand::from_seed(structure_seed as u64))
|
||||
}
|
||||
|
||||
pub enum StructureInstance {
|
||||
@@ -696,3 +701,29 @@ pub enum StructureInstance {
|
||||
/// Stores the `BlockPos` of the 'Start' so you can look it up.
|
||||
Reference(Arc<Mutex<StructurePiecesCollector>>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod structure_random_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn large_feature_seed_matches_java_random() {
|
||||
let mut random = create_chunk_random(123_456_789, -37, 84);
|
||||
assert_eq!(
|
||||
[
|
||||
random.next_i32(),
|
||||
random.next_i32(),
|
||||
random.next_i32(),
|
||||
random.next_i32(),
|
||||
random.next_i32(),
|
||||
],
|
||||
[
|
||||
-2_113_851_872,
|
||||
-821_770_162,
|
||||
381_681_559,
|
||||
-196_012_664,
|
||||
372_718_864
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ impl StructurePieceBase for NetherFossilPiece {
|
||||
(0, 0),
|
||||
self.rotation,
|
||||
true,
|
||||
false,
|
||||
&[],
|
||||
Some(_chunk_box),
|
||||
);
|
||||
|
||||
@@ -46,17 +46,26 @@ impl BlockStateResolver {
|
||||
}
|
||||
|
||||
// Transform properties for rotation/mirror
|
||||
let transformed_props: Vec<(&str, &str)> = entry
|
||||
let transformed_props: Vec<(String, String)> = entry
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
let transformed_key = match key.as_str() {
|
||||
"north" | "south" | "east" | "west" => rotation
|
||||
.rotate_facing(mirror.mirror_facing(key))
|
||||
.to_string(),
|
||||
_ => key.clone(),
|
||||
};
|
||||
let new_value = Self::transform_property(key, value, rotation, mirror);
|
||||
(key.as_str(), new_value)
|
||||
(transformed_key, new_value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Convert to the format expected by from_properties
|
||||
let props_slice: Vec<(&str, &str)> = transformed_props;
|
||||
let props_slice = transformed_props
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Get the state ID from properties
|
||||
let props_box = block.from_properties(&props_slice);
|
||||
@@ -72,21 +81,16 @@ impl BlockStateResolver {
|
||||
}
|
||||
|
||||
/// Transforms a property value based on rotation and mirror.
|
||||
fn transform_property(
|
||||
key: &str,
|
||||
value: &str,
|
||||
rotation: Rotation,
|
||||
mirror: Mirror,
|
||||
) -> &'static str {
|
||||
fn transform_property(key: &str, value: &str, rotation: Rotation, mirror: Mirror) -> String {
|
||||
match key {
|
||||
// Horizontal facing properties
|
||||
"facing" => {
|
||||
let mirrored = mirror.mirror_facing(value);
|
||||
rotation.rotate_facing(mirrored)
|
||||
rotation.rotate_facing(mirrored).to_string()
|
||||
}
|
||||
|
||||
// Axis properties (for logs, pillars, etc.)
|
||||
"axis" => rotation.rotate_axis(value),
|
||||
"axis" => rotation.rotate_axis(value).to_string(),
|
||||
|
||||
// Block rotation (signs, banners - 0-15 value)
|
||||
"rotation" => {
|
||||
@@ -94,15 +98,15 @@ impl BlockStateResolver {
|
||||
let mirrored = mirror.mirror_block_rotation(rot_value);
|
||||
let rotated = rotation.rotate_block_rotation(mirrored);
|
||||
// Use static strings for the 16 possible rotation values
|
||||
Self::rotation_to_str(rotated)
|
||||
Self::rotation_to_str(rotated).to_string()
|
||||
} else {
|
||||
Self::leak_str(value)
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Half properties don't need rotation (top/bottom stays the same)
|
||||
// Shape, mode, and most other properties don't need transformation either
|
||||
_ => Self::leak_str(value),
|
||||
_ => value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,56 +132,6 @@ impl BlockStateResolver {
|
||||
_ => "0",
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a string to a 'static str by leaking it.
|
||||
/// This is necessary for returning transformed property values.
|
||||
fn leak_str(s: &str) -> &'static str {
|
||||
// For common values, return static strings to avoid leaking
|
||||
match s {
|
||||
"north" => "north",
|
||||
"south" => "south",
|
||||
"east" => "east",
|
||||
"west" => "west",
|
||||
"up" => "up",
|
||||
"down" => "down",
|
||||
"x" => "x",
|
||||
"y" => "y",
|
||||
"z" => "z",
|
||||
"true" => "true",
|
||||
"false" => "false",
|
||||
"top" => "top",
|
||||
"bottom" => "bottom",
|
||||
"upper" => "upper",
|
||||
"lower" => "lower",
|
||||
"straight" => "straight",
|
||||
"inner_left" => "inner_left",
|
||||
"inner_right" => "inner_right",
|
||||
"outer_left" => "outer_left",
|
||||
"outer_right" => "outer_right",
|
||||
"0" => "0",
|
||||
"1" => "1",
|
||||
"2" => "2",
|
||||
"3" => "3",
|
||||
"4" => "4",
|
||||
"5" => "5",
|
||||
"6" => "6",
|
||||
"7" => "7",
|
||||
"8" => "8",
|
||||
"9" => "9",
|
||||
"10" => "10",
|
||||
"11" => "11",
|
||||
"12" => "12",
|
||||
"13" => "13",
|
||||
"14" => "14",
|
||||
"15" => "15",
|
||||
"head" => "head",
|
||||
"foot" => "foot",
|
||||
"single" => "single",
|
||||
"left" => "left",
|
||||
"right" => "right",
|
||||
_ => s.to_string().leak(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -14,7 +14,7 @@ use super::{StructureTemplate, structure_template::TemplateError};
|
||||
/// Templates are loaded lazily on first access and stored for reuse.
|
||||
/// The cache is thread-safe and can be accessed from multiple threads.
|
||||
pub struct TemplateCache {
|
||||
cache: DashMap<&'static str, Arc<StructureTemplate>>,
|
||||
cache: DashMap<String, Arc<StructureTemplate>>,
|
||||
}
|
||||
|
||||
impl Default for TemplateCache {
|
||||
@@ -36,7 +36,9 @@ impl TemplateCache {
|
||||
///
|
||||
/// Returns the loaded template wrapped in an `Arc`, or `None` if the template
|
||||
/// doesn't exist or failed to load.
|
||||
pub fn get(&self, name: &'static str) -> Option<Arc<StructureTemplate>> {
|
||||
pub fn get(&self, name: &str) -> Option<Arc<StructureTemplate>> {
|
||||
let name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
|
||||
// Check cache first
|
||||
if let Some(template) = self.cache.get(name) {
|
||||
return Some(Arc::clone(&template));
|
||||
@@ -48,7 +50,7 @@ impl TemplateCache {
|
||||
match StructureTemplate::from_nbt_bytes(bytes) {
|
||||
Ok(template) => {
|
||||
let arc = Arc::new(template);
|
||||
self.cache.insert(name, Arc::clone(&arc));
|
||||
self.cache.insert(name.to_owned(), Arc::clone(&arc));
|
||||
Some(arc)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -63,10 +65,9 @@ impl TemplateCache {
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the template doesn't exist or fails to parse.
|
||||
pub fn get_or_error(
|
||||
&self,
|
||||
name: &'static str,
|
||||
) -> Result<Arc<StructureTemplate>, TemplateError> {
|
||||
pub fn get_or_error(&self, name: &str) -> Result<Arc<StructureTemplate>, TemplateError> {
|
||||
let name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
|
||||
// Check cache first
|
||||
if let Some(template) = self.cache.get(name) {
|
||||
return Ok(Arc::clone(&template));
|
||||
@@ -78,7 +79,7 @@ impl TemplateCache {
|
||||
|
||||
let template = StructureTemplate::from_nbt_bytes(bytes)?;
|
||||
let arc = Arc::new(template);
|
||||
self.cache.insert(name, Arc::clone(&arc));
|
||||
self.cache.insert(name.to_owned(), Arc::clone(&arc));
|
||||
Ok(arc)
|
||||
}
|
||||
|
||||
@@ -139,6 +140,6 @@ pub fn global_cache() -> &'static TemplateCache {
|
||||
///
|
||||
/// Returns the loaded template wrapped in an `Arc`, or `None` if not found.
|
||||
#[must_use]
|
||||
pub fn get_template(name: &'static str) -> Option<Arc<StructureTemplate>> {
|
||||
pub fn get_template(name: &str) -> Option<Arc<StructureTemplate>> {
|
||||
global_cache().get(name)
|
||||
}
|
||||
|
||||
@@ -33,11 +33,15 @@ mod template_piece;
|
||||
use pumpkin_data::Rotation;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_util::random::{RandomImpl, hash_block_pos, legacy_rand::LegacyRand};
|
||||
|
||||
use crate::ProtoChunk;
|
||||
|
||||
pub use block_state_resolver::BlockStateResolver;
|
||||
pub use cache::{TemplateCache, get_pool_elements, get_template, global_cache};
|
||||
pub use cache::{
|
||||
TemplateCache, get_pool_elements, get_processor_list_json, get_template,
|
||||
get_template_pool_json, global_cache,
|
||||
};
|
||||
pub use processor::StructureProcessor;
|
||||
pub use pumpkin_data::{Mirror as BlockMirror, Rotation as BlockRotation};
|
||||
pub use structure_template::{PaletteEntry, StructureTemplate, TemplateBlock, TemplateEntity};
|
||||
@@ -61,7 +65,8 @@ pub fn place_template(
|
||||
offset: (i32, i32),
|
||||
rotation: Rotation,
|
||||
skip_air: bool,
|
||||
processors: &[Box<dyn StructureProcessor>],
|
||||
apply_waterlogging: bool,
|
||||
processors: &[StructureProcessor],
|
||||
chunk_box: Option<&pumpkin_util::math::block_box::BlockBox>,
|
||||
) {
|
||||
let (rotated_ox, rotated_oz) = rotation.rotate_offset(offset.0, offset.1);
|
||||
@@ -71,8 +76,10 @@ pub fn place_template(
|
||||
for block in &template.blocks {
|
||||
let palette_entry = &template.palette[block.state as usize];
|
||||
|
||||
// Skip structure void blocks
|
||||
if palette_entry.name == "minecraft:structure_void" {
|
||||
// Structure blocks are data markers and structure void preserves the existing block.
|
||||
if palette_entry.name == "minecraft:structure_void"
|
||||
|| palette_entry.name == "minecraft:structure_block"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -81,9 +88,23 @@ pub fn place_template(
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut block_entity_nbt = block.nbt.clone();
|
||||
let mut placed_entry = palette_entry.clone();
|
||||
|
||||
// Jigsaw blocks are replaced during template processing, before block entities are
|
||||
// collected. Keeping this in the placement pipeline avoids stale jigsaw entities.
|
||||
if palette_entry.name == "minecraft:jigsaw" {
|
||||
let final_state = block_entity_nbt
|
||||
.as_ref()
|
||||
.and_then(|nbt| nbt.get_string("final_state"))
|
||||
.unwrap_or("minecraft:air");
|
||||
placed_entry = PaletteEntry::from_string(final_state);
|
||||
block_entity_nbt = None;
|
||||
}
|
||||
|
||||
// Resolve block state with rotation applied to directional properties
|
||||
let Some(mut state) =
|
||||
BlockStateResolver::resolve(palette_entry, rotation, Default::default())
|
||||
BlockStateResolver::resolve(&placed_entry, rotation, Default::default())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -108,39 +129,67 @@ pub fn place_template(
|
||||
|
||||
let world_pos = Vector3::new(wx, wy, wz);
|
||||
|
||||
if apply_waterlogging
|
||||
&& chunk.get_block_state(&world_pos).to_block_id() == pumpkin_data::Block::WATER.id
|
||||
&& let Some((_, waterlogged)) = placed_entry
|
||||
.properties
|
||||
.iter_mut()
|
||||
.find(|(name, _)| name == "waterlogged")
|
||||
{
|
||||
*waterlogged = "true".to_string();
|
||||
if let Some(waterlogged_state) =
|
||||
BlockStateResolver::resolve(&placed_entry, rotation, Default::default())
|
||||
{
|
||||
state = waterlogged_state;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply processors
|
||||
let mut should_place = true;
|
||||
for processor in processors {
|
||||
state = processor.process(chunk, world_pos, state);
|
||||
let Some(processed_state) = processor.process(chunk, world_pos, state) else {
|
||||
should_place = false;
|
||||
break;
|
||||
};
|
||||
state = processed_state;
|
||||
}
|
||||
if !should_place {
|
||||
continue;
|
||||
}
|
||||
|
||||
chunk.set_block_state(wx, wy, wz, state);
|
||||
|
||||
// Create block entities for interactive blocks (furnaces, chests, etc.)
|
||||
let block_entity_id = get_block_entity_id(&palette_entry.name);
|
||||
if block.nbt.is_some() || block_entity_id.is_some() {
|
||||
let block_entity_id = block_entity_id.unwrap_or(&palette_entry.name);
|
||||
let mut block_entity_nbt = NbtCompound::new();
|
||||
let block_entity_id = get_block_entity_id(&placed_entry.name);
|
||||
if block_entity_nbt.is_some() || block_entity_id.is_some() {
|
||||
let block_entity_id = block_entity_id.unwrap_or(&placed_entry.name);
|
||||
let mut placed_nbt = NbtCompound::new();
|
||||
|
||||
block_entity_nbt.put_string("id", block_entity_id.to_string());
|
||||
block_entity_nbt.put_int("x", wx);
|
||||
block_entity_nbt.put_int("y", wy);
|
||||
block_entity_nbt.put_int("z", wz);
|
||||
placed_nbt.put_string("id", block_entity_id.to_string());
|
||||
placed_nbt.put_int("x", wx);
|
||||
placed_nbt.put_int("y", wy);
|
||||
placed_nbt.put_int("z", wz);
|
||||
|
||||
if let Some(template_nbt) = &block.nbt {
|
||||
if let Some(template_nbt) = &block_entity_nbt {
|
||||
for (key, value) in &template_nbt.child_tags {
|
||||
if key.as_ref() != "x"
|
||||
&& key.as_ref() != "y"
|
||||
&& key.as_ref() != "z"
|
||||
&& key.as_ref() != "id"
|
||||
{
|
||||
block_entity_nbt
|
||||
.child_tags
|
||||
.insert(key.clone(), value.clone());
|
||||
placed_nbt.child_tags.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunk.add_block_entity(block_entity_nbt);
|
||||
if placed_nbt.get_string("LootTable").is_some()
|
||||
&& placed_nbt.get_long("LootTableSeed").is_none()
|
||||
{
|
||||
let mut random = LegacyRand::from_seed(hash_block_pos(wx, wy, wz) as u64);
|
||||
placed_nbt.put_long("LootTableSeed", random.next_i64());
|
||||
}
|
||||
|
||||
chunk.add_block_entity(placed_nbt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,209 @@
|
||||
use pumpkin_data::{Block, BlockState};
|
||||
use pumpkin_util::{
|
||||
math::vector3::Vector3,
|
||||
random::{RandomImpl, hash_block_pos, legacy_rand::LegacyRand},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::ProtoChunk;
|
||||
use pumpkin_data::BlockState;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
|
||||
pub trait StructureProcessor: Send + Sync {
|
||||
fn process(
|
||||
&self,
|
||||
chunk: &mut ProtoChunk,
|
||||
pos: Vector3<i32>,
|
||||
state: &'static BlockState,
|
||||
) -> &'static BlockState;
|
||||
#[derive(Clone)]
|
||||
pub enum StructureProcessor {
|
||||
BlockRot { integrity: f32, blocks: BlockTag },
|
||||
Rules(Vec<ProcessorRule>),
|
||||
ProtectedBlocks(BlockTag),
|
||||
}
|
||||
|
||||
pub struct GravityProcessor {
|
||||
pub heightmap: pumpkin_util::HeightMap,
|
||||
pub offset: i32,
|
||||
#[derive(Clone)]
|
||||
pub struct ProcessorRule {
|
||||
input_block: u16,
|
||||
probability: f32,
|
||||
output_state: &'static BlockState,
|
||||
}
|
||||
|
||||
impl StructureProcessor for GravityProcessor {
|
||||
fn process(
|
||||
&self,
|
||||
chunk: &mut ProtoChunk,
|
||||
pos: Vector3<i32>,
|
||||
state: &'static BlockState,
|
||||
) -> &'static BlockState {
|
||||
let top_y = chunk.get_top_y(&self.heightmap, pos.x, pos.z);
|
||||
let _target_y = top_y + self.offset;
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum BlockTag {
|
||||
AncientCityReplaceable,
|
||||
FeaturesCannotReplace,
|
||||
}
|
||||
|
||||
// For now, just a simple gravity: if we are placing above the ground, shift it down?
|
||||
// Actually, GravityProcessor in vanilla is used to place blocks relative to the ground.
|
||||
// But our jigsaw already adjusted the piece origin.
|
||||
impl BlockTag {
|
||||
fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"#minecraft:ancient_city_replaceable" => Some(Self::AncientCityReplaceable),
|
||||
"#minecraft:features_cannot_replace" => Some(Self::FeaturesCannotReplace),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let block = pumpkin_data::Block::from_state_id(state.id);
|
||||
if block.name == "cobblestone" || block.name == "stone_bricks" {
|
||||
// Fill down to ground
|
||||
for y in (top_y..pos.y).rev() {
|
||||
chunk.set_block_state(pos.x, y, pos.z, state);
|
||||
fn contains(self, block_id: u16) -> bool {
|
||||
match self {
|
||||
Self::AncientCityReplaceable => {
|
||||
pumpkin_data::tag::Block::MINECRAFT_ANCIENT_CITY_REPLACEABLE
|
||||
.1
|
||||
.contains(&block_id)
|
||||
}
|
||||
Self::FeaturesCannotReplace => {
|
||||
pumpkin_data::tag::Block::MINECRAFT_FEATURES_CANNOT_REPLACE
|
||||
.1
|
||||
.contains(&block_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
impl StructureProcessor {
|
||||
#[must_use]
|
||||
pub fn process(
|
||||
&self,
|
||||
chunk: &ProtoChunk,
|
||||
pos: Vector3<i32>,
|
||||
state: &'static BlockState,
|
||||
) -> Option<&'static BlockState> {
|
||||
let input_block = Block::get_raw_id_from_state_id(state.id);
|
||||
match self {
|
||||
Self::BlockRot { integrity, blocks } => {
|
||||
if !blocks.contains(input_block) {
|
||||
return Some(state);
|
||||
}
|
||||
let mut random = LegacyRand::from_seed(hash_block_pos(pos.x, pos.y, pos.z) as u64);
|
||||
(random.next_f32() <= *integrity).then_some(state)
|
||||
}
|
||||
Self::Rules(rules) => {
|
||||
let mut random = LegacyRand::from_seed(hash_block_pos(pos.x, pos.y, pos.z) as u64);
|
||||
rules
|
||||
.iter()
|
||||
.find(|rule| {
|
||||
input_block == rule.input_block && random.next_f32() < rule.probability
|
||||
})
|
||||
.map_or(Some(state), |rule| Some(rule.output_state))
|
||||
}
|
||||
Self::ProtectedBlocks(blocks) => {
|
||||
let existing = chunk.get_block_state(&pos).to_block_id();
|
||||
(!blocks.contains(existing)).then_some(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawProcessorList {
|
||||
processors: Vec<RawProcessor>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "processor_type")]
|
||||
enum RawProcessor {
|
||||
#[serde(rename = "minecraft:block_rot")]
|
||||
BlockRot {
|
||||
integrity: f32,
|
||||
rottable_blocks: String,
|
||||
},
|
||||
#[serde(rename = "minecraft:rule")]
|
||||
Rule { rules: Vec<RawRule> },
|
||||
#[serde(rename = "minecraft:protected_blocks")]
|
||||
ProtectedBlocks { value: String },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawRule {
|
||||
input_predicate: RawInputPredicate,
|
||||
output_state: RawOutputState,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawInputPredicate {
|
||||
block: String,
|
||||
probability: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawOutputState {
|
||||
#[serde(rename = "Name")]
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn load_processor_list(name: &str) -> Arc<[StructureProcessor]> {
|
||||
static CACHE: LazyLock<dashmap::DashMap<String, Arc<[StructureProcessor]>>> =
|
||||
LazyLock::new(dashmap::DashMap::new);
|
||||
|
||||
if let Some(processors) = CACHE.get(name) {
|
||||
return Arc::clone(&processors);
|
||||
}
|
||||
|
||||
let Some(json) = super::cache::get_processor_list_json(name) else {
|
||||
tracing::warn!("Unknown structure processor list: {name}");
|
||||
return Arc::from([]);
|
||||
};
|
||||
let raw: RawProcessorList = match serde_json::from_str(json) {
|
||||
Ok(raw) => raw,
|
||||
Err(error) => {
|
||||
tracing::error!("Failed to parse structure processor list {name}: {error}");
|
||||
return Arc::from([]);
|
||||
}
|
||||
};
|
||||
|
||||
let processors = raw
|
||||
.processors
|
||||
.into_iter()
|
||||
.filter_map(|processor| match processor {
|
||||
RawProcessor::BlockRot {
|
||||
integrity,
|
||||
rottable_blocks,
|
||||
} => BlockTag::from_name(&rottable_blocks)
|
||||
.map(|blocks| StructureProcessor::BlockRot { integrity, blocks }),
|
||||
RawProcessor::ProtectedBlocks { value } => {
|
||||
BlockTag::from_name(&value).map(StructureProcessor::ProtectedBlocks)
|
||||
}
|
||||
RawProcessor::Rule { rules } => Some(StructureProcessor::Rules(
|
||||
rules
|
||||
.into_iter()
|
||||
.filter_map(|rule| {
|
||||
let input_name = rule
|
||||
.input_predicate
|
||||
.block
|
||||
.strip_prefix("minecraft:")
|
||||
.unwrap_or(&rule.input_predicate.block);
|
||||
let output_name = rule
|
||||
.output_state
|
||||
.name
|
||||
.strip_prefix("minecraft:")
|
||||
.unwrap_or(&rule.output_state.name);
|
||||
let input_block = Block::from_name(input_name)?;
|
||||
let output_block = Block::from_name(output_name)?;
|
||||
Some(ProcessorRule {
|
||||
input_block: input_block.id,
|
||||
probability: rule.input_predicate.probability,
|
||||
output_state: output_block.default_state,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
})
|
||||
.collect::<Arc<[_]>>();
|
||||
CACHE.insert(name.to_owned(), Arc::clone(&processors));
|
||||
processors
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_ancient_city_processor_lists() {
|
||||
assert_eq!(
|
||||
load_processor_list("minecraft:ancient_city_generic_degradation").len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
load_processor_list("minecraft:ancient_city_start_degradation").len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
load_processor_list("minecraft:ancient_city_walls_degradation").len(),
|
||||
3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,28 +113,32 @@ impl JigsawBlockEntity {
|
||||
let mut pieces = std::mem::take(&mut structure.collector.lock().unwrap().pieces);
|
||||
for piece in &mut pieces {
|
||||
if let Some(pool_piece) = piece.as_any().downcast_ref::<PoolElementStructurePiece>() {
|
||||
let template = Arc::clone(&pool_piece.template);
|
||||
let origin = pool_piece.pos;
|
||||
let rotation = pool_piece.rotation;
|
||||
let mut templates = Vec::new();
|
||||
pool_piece
|
||||
.element
|
||||
.for_each_template(|_, _, template| templates.push(template));
|
||||
|
||||
for block in &template.blocks {
|
||||
let palette_entry = &template.palette[block.state as usize];
|
||||
if palette_entry.name == "minecraft:structure_void" {
|
||||
continue;
|
||||
}
|
||||
for template in templates {
|
||||
for block in &template.blocks {
|
||||
let palette_entry = &template.palette[block.state as usize];
|
||||
if palette_entry.name == "minecraft:structure_void" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !keep_jigsaws && palette_entry.name == "minecraft:jigsaw" {
|
||||
let final_state_str = block
|
||||
.nbt
|
||||
.as_ref()
|
||||
.and_then(|n| n.get_string("final_state"))
|
||||
.unwrap_or("minecraft:air");
|
||||
if !keep_jigsaws && palette_entry.name == "minecraft:jigsaw" {
|
||||
let final_state_str = block
|
||||
.nbt
|
||||
.as_ref()
|
||||
.and_then(|n| n.get_string("final_state"))
|
||||
.unwrap_or("minecraft:air");
|
||||
|
||||
let entry =
|
||||
let entry =
|
||||
pumpkin_world::generation::structure::template::PaletteEntry::from_string(
|
||||
final_state_str,
|
||||
);
|
||||
let final_state =
|
||||
let final_state =
|
||||
pumpkin_world::generation::structure::template::BlockStateResolver::resolve(
|
||||
&entry,
|
||||
rotation,
|
||||
@@ -142,48 +146,49 @@ impl JigsawBlockEntity {
|
||||
)
|
||||
.unwrap_or(pumpkin_data::Block::AIR.default_state);
|
||||
|
||||
let local_pos = rotation.transform_pos(block.pos, template.size);
|
||||
let world_pos = origin.add(local_pos.x, local_pos.y, local_pos.z);
|
||||
|
||||
world
|
||||
.set_block_state(
|
||||
&world_pos,
|
||||
final_state.id,
|
||||
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
|
||||
)
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = BlockStateResolver::resolve(
|
||||
palette_entry,
|
||||
rotation,
|
||||
pumpkin_data::Mirror::default(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let local_pos = rotation.transform_pos(block.pos, template.size);
|
||||
let world_pos = origin.add(local_pos.x, local_pos.y, local_pos.z);
|
||||
|
||||
world
|
||||
.set_block_state(
|
||||
&world_pos,
|
||||
final_state.id,
|
||||
state.id,
|
||||
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
|
||||
)
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = BlockStateResolver::resolve(
|
||||
palette_entry,
|
||||
rotation,
|
||||
pumpkin_data::Mirror::default(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let local_pos = rotation.transform_pos(block.pos, template.size);
|
||||
let world_pos = origin.add(local_pos.x, local_pos.y, local_pos.z);
|
||||
|
||||
world
|
||||
.set_block_state(
|
||||
&world_pos,
|
||||
state.id,
|
||||
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Handle block entities
|
||||
if let Some(nbt) = &block.nbt {
|
||||
let mut block_entity_nbt = nbt.clone();
|
||||
block_entity_nbt.put_int("x", world_pos.0.x);
|
||||
block_entity_nbt.put_int("y", world_pos.0.y);
|
||||
block_entity_nbt.put_int("z", world_pos.0.z);
|
||||
if let Some(block_entity) =
|
||||
crate::block::entities::block_entity_from_nbt(&block_entity_nbt)
|
||||
{
|
||||
world.add_block_entity(block_entity);
|
||||
// Handle block entities
|
||||
if let Some(nbt) = &block.nbt {
|
||||
let mut block_entity_nbt = nbt.clone();
|
||||
block_entity_nbt.put_int("x", world_pos.0.x);
|
||||
block_entity_nbt.put_int("y", world_pos.0.y);
|
||||
block_entity_nbt.put_int("z", world_pos.0.z);
|
||||
if let Some(block_entity) =
|
||||
crate::block::entities::block_entity_from_nbt(&block_entity_nbt)
|
||||
{
|
||||
world.add_block_entity(block_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user