diff --git a/pumpkin-codegen/src/placed_feature.rs b/pumpkin-codegen/src/placed_feature.rs index 4696dbb1d..aa6d3d98c 100644 --- a/pumpkin-codegen/src/placed_feature.rs +++ b/pumpkin-codegen/src/placed_feature.rs @@ -13,6 +13,7 @@ pub fn build_enum() -> TokenStream { let mut from_name_arms = Vec::new(); let mut to_name_arms = Vec::new(); + let mut all_names: Vec = Vec::new(); let variants: Vec = json .as_object() @@ -26,12 +27,18 @@ pub fn build_enum() -> TokenStream { to_name_arms.push(quote! { Self::#variant_name => #name, }); + all_names.push(format!("minecraft:{name}")); quote! { #variant_name, } }) .collect(); + let all_names_tokens: Vec = all_names + .iter() + .map(|name| quote! { #name }) + .collect(); + quote! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum PlacedFeature { @@ -52,6 +59,11 @@ pub fn build_enum() -> TokenStream { #(#to_name_arms)* } } + + #[must_use] + pub const fn all_names() -> &'static [&'static str] { + &[#(#all_names_tokens),*] + } } } } diff --git a/pumpkin-codegen/src/structures.rs b/pumpkin-codegen/src/structures.rs index c24ed9e45..d7adb37c8 100644 --- a/pumpkin-codegen/src/structures.rs +++ b/pumpkin-codegen/src/structures.rs @@ -1,3 +1,4 @@ +use heck::ToPascalCase; use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; use serde::Deserialize; @@ -429,11 +430,15 @@ pub fn build() -> TokenStream { let mut structure_const_defs = TokenStream::new(); let mut structure_lookup_arms = TokenStream::new(); + let mut structure_from_name_arms = Vec::new(); + let mut structure_to_name_arms = Vec::new(); + let mut structure_all_names: Vec = Vec::new(); for (name, structure) in &structures_json { let stripped_name = name.strip_prefix("minecraft:").unwrap_or(name); let upper_name = stripped_name.to_uppercase(); let const_name = format_ident!("{}", upper_name); + let variant_ident = format_ident!("{}", stripped_name.to_pascal_case()); let key_variant = structure_key_to_token(name); structure_const_defs.extend(quote!( @@ -443,6 +448,14 @@ pub fn build() -> TokenStream { structure_lookup_arms.extend(quote!( #key_variant => &Self::#const_name, )); + + structure_from_name_arms.push(quote! { + #stripped_name => Some(Self::#variant_ident), + }); + structure_to_name_arms.push(quote! { + Self::#variant_ident => #name, + }); + structure_all_names.push(stripped_name.to_string()); } let mut structure_set_const_defs = TokenStream::new(); @@ -465,6 +478,14 @@ pub fn build() -> TokenStream { all_structure_set_idents.push(const_name); } + let structure_all_names_tokens: Vec = structure_all_names + .iter() + .map(|name| { + let full = format!("minecraft:{name}"); + quote! { #full } + }) + .collect(); + quote!( use pumpkin_util::math::floor_div; use pumpkin_util::random::{ @@ -510,6 +531,28 @@ pub fn build() -> TokenStream { TrialChambers, } + impl StructureKeys { + pub fn from_name(name: &str) -> Option { + let name = name.strip_prefix("minecraft:").unwrap_or(name); + match name { + #(#structure_from_name_arms)* + _ => None, + } + } + + #[must_use] + pub const fn to_name(&self) -> &'static str { + match self { + #(#structure_to_name_arms)* + } + } + + #[must_use] + pub const fn all_names() -> &'static [&'static str] { + &[#(#structure_all_names_tokens),*] + } + } + pub struct StructureSet { pub placement: StructurePlacement, pub structures: &'static [WeightedEntry], diff --git a/pumpkin-data/src/generated/placed_feature.rs b/pumpkin-data/src/generated/placed_feature.rs index f99a13c50..970669689 100644 --- a/pumpkin-data/src/generated/placed_feature.rs +++ b/pumpkin-data/src/generated/placed_feature.rs @@ -799,4 +799,271 @@ impl PlacedFeature { Self::WildflowersMeadow => "wildflowers_meadow", } } + #[must_use] + pub const fn all_names() -> &'static [&'static str] { + &[ + "minecraft:acacia", + "minecraft:acacia_checked", + "minecraft:amethyst_geode", + "minecraft:bamboo", + "minecraft:bamboo_light", + "minecraft:bamboo_vegetation", + "minecraft:basalt_blobs", + "minecraft:basalt_pillar", + "minecraft:birch_bees_0002", + "minecraft:birch_bees_0002_leaf_litter", + "minecraft:birch_bees_002", + "minecraft:birch_checked", + "minecraft:birch_leaf_litter", + "minecraft:birch_tall", + "minecraft:blackstone_blobs", + "minecraft:blue_ice", + "minecraft:brown_mushroom_nether", + "minecraft:brown_mushroom_normal", + "minecraft:brown_mushroom_old_growth", + "minecraft:brown_mushroom_swamp", + "minecraft:brown_mushroom_taiga", + "minecraft:cave_vines", + "minecraft:cherry_bees_005", + "minecraft:cherry_checked", + "minecraft:chorus_plant", + "minecraft:classic_vines_cave_feature", + "minecraft:crimson_forest_vegetation", + "minecraft:crimson_fungi", + "minecraft:dark_forest_vegetation", + "minecraft:dark_oak_checked", + "minecraft:dark_oak_leaf_litter", + "minecraft:delta", + "minecraft:desert_well", + "minecraft:disk_clay", + "minecraft:disk_grass", + "minecraft:disk_gravel", + "minecraft:disk_sand", + "minecraft:dripstone_cluster", + "minecraft:end_gateway_return", + "minecraft:end_island_decorated", + "minecraft:end_platform", + "minecraft:end_spike", + "minecraft:fallen_birch_tree", + "minecraft:fallen_jungle_tree", + "minecraft:fallen_oak_tree", + "minecraft:fallen_spruce_tree", + "minecraft:fallen_super_birch_tree", + "minecraft:fancy_oak_bees", + "minecraft:fancy_oak_bees_0002_leaf_litter", + "minecraft:fancy_oak_bees_002", + "minecraft:fancy_oak_checked", + "minecraft:fancy_oak_leaf_litter", + "minecraft:flower_cherry", + "minecraft:flower_default", + "minecraft:flower_flower_forest", + "minecraft:flower_forest_flowers", + "minecraft:flower_meadow", + "minecraft:flower_pale_garden", + "minecraft:flower_plain", + "minecraft:flower_plains", + "minecraft:flower_swamp", + "minecraft:flower_warm", + "minecraft:forest_flowers", + "minecraft:forest_rock", + "minecraft:fossil_lower", + "minecraft:fossil_upper", + "minecraft:freeze_top_layer", + "minecraft:glow_lichen", + "minecraft:glowstone", + "minecraft:glowstone_extra", + "minecraft:grass_bonemeal", + "minecraft:ice_patch", + "minecraft:ice_spike", + "minecraft:iceberg_blue", + "minecraft:iceberg_packed", + "minecraft:jungle_bush", + "minecraft:jungle_tree", + "minecraft:kelp_cold", + "minecraft:kelp_warm", + "minecraft:lake_lava_surface", + "minecraft:lake_lava_underground", + "minecraft:large_basalt_columns", + "minecraft:large_dripstone", + "minecraft:lush_caves_ceiling_vegetation", + "minecraft:lush_caves_clay", + "minecraft:lush_caves_vegetation", + "minecraft:mangrove_checked", + "minecraft:mega_jungle_tree_checked", + "minecraft:mega_pine_checked", + "minecraft:mega_spruce_checked", + "minecraft:monster_room", + "minecraft:monster_room_deep", + "minecraft:mushroom_island_vegetation", + "minecraft:nether_sprouts", + "minecraft:oak", + "minecraft:oak_bees_0002_leaf_litter", + "minecraft:oak_bees_002", + "minecraft:oak_checked", + "minecraft:oak_leaf_litter", + "minecraft:ore_ancient_debris_large", + "minecraft:ore_andesite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_blackstone", + "minecraft:ore_clay", + "minecraft:ore_coal_lower", + "minecraft:ore_coal_upper", + "minecraft:ore_copper", + "minecraft:ore_copper_large", + "minecraft:ore_debris_small", + "minecraft:ore_diamond", + "minecraft:ore_diamond_buried", + "minecraft:ore_diamond_large", + "minecraft:ore_diamond_medium", + "minecraft:ore_diorite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_dirt", + "minecraft:ore_emerald", + "minecraft:ore_gold", + "minecraft:ore_gold_deltas", + "minecraft:ore_gold_extra", + "minecraft:ore_gold_lower", + "minecraft:ore_gold_nether", + "minecraft:ore_granite_lower", + "minecraft:ore_granite_upper", + "minecraft:ore_gravel", + "minecraft:ore_gravel_nether", + "minecraft:ore_infested", + "minecraft:ore_iron_middle", + "minecraft:ore_iron_small", + "minecraft:ore_iron_upper", + "minecraft:ore_lapis", + "minecraft:ore_lapis_buried", + "minecraft:ore_magma", + "minecraft:ore_quartz_deltas", + "minecraft:ore_quartz_nether", + "minecraft:ore_redstone", + "minecraft:ore_redstone_lower", + "minecraft:ore_soul_sand", + "minecraft:ore_tuff", + "minecraft:pale_garden_flowers", + "minecraft:pale_garden_vegetation", + "minecraft:pale_moss_patch", + "minecraft:pale_oak_checked", + "minecraft:pale_oak_creaking_checked", + "minecraft:patch_berry_bush", + "minecraft:patch_berry_common", + "minecraft:patch_berry_rare", + "minecraft:patch_bush", + "minecraft:patch_cactus", + "minecraft:patch_cactus_decorated", + "minecraft:patch_cactus_desert", + "minecraft:patch_crimson_roots", + "minecraft:patch_dead_bush", + "minecraft:patch_dead_bush_2", + "minecraft:patch_dead_bush_badlands", + "minecraft:patch_dry_grass_badlands", + "minecraft:patch_dry_grass_desert", + "minecraft:patch_fire", + "minecraft:patch_firefly_bush_near_water", + "minecraft:patch_firefly_bush_near_water_swamp", + "minecraft:patch_firefly_bush_swamp", + "minecraft:patch_grass_badlands", + "minecraft:patch_grass_forest", + "minecraft:patch_grass_jungle", + "minecraft:patch_grass_meadow", + "minecraft:patch_grass_normal", + "minecraft:patch_grass_plain", + "minecraft:patch_grass_savanna", + "minecraft:patch_grass_taiga", + "minecraft:patch_grass_taiga_2", + "minecraft:patch_large_fern", + "minecraft:patch_leaf_litter", + "minecraft:patch_melon", + "minecraft:patch_melon_sparse", + "minecraft:patch_pumpkin", + "minecraft:patch_soul_fire", + "minecraft:patch_sugar_cane", + "minecraft:patch_sugar_cane_badlands", + "minecraft:patch_sugar_cane_desert", + "minecraft:patch_sugar_cane_swamp", + "minecraft:patch_sunflower", + "minecraft:patch_taiga_grass", + "minecraft:patch_tall_grass", + "minecraft:patch_tall_grass_2", + "minecraft:patch_waterlily", + "minecraft:pile_hay", + "minecraft:pile_ice", + "minecraft:pile_melon", + "minecraft:pile_pumpkin", + "minecraft:pile_snow", + "minecraft:pine", + "minecraft:pine_checked", + "minecraft:pine_on_snow", + "minecraft:pointed_dripstone", + "minecraft:red_mushroom_nether", + "minecraft:red_mushroom_normal", + "minecraft:red_mushroom_old_growth", + "minecraft:red_mushroom_swamp", + "minecraft:red_mushroom_taiga", + "minecraft:rooted_azalea_tree", + "minecraft:rooted_sulfur_spring", + "minecraft:sculk_patch_ancient_city", + "minecraft:sculk_patch_deep_dark", + "minecraft:sculk_vein", + "minecraft:sea_pickle", + "minecraft:seagrass_cold", + "minecraft:seagrass_deep", + "minecraft:seagrass_deep_cold", + "minecraft:seagrass_deep_warm", + "minecraft:seagrass_normal", + "minecraft:seagrass_river", + "minecraft:seagrass_swamp", + "minecraft:seagrass_warm", + "minecraft:small_basalt_columns", + "minecraft:spore_blossom", + "minecraft:spring_closed", + "minecraft:spring_closed_double", + "minecraft:spring_delta", + "minecraft:spring_lava", + "minecraft:spring_lava_frozen", + "minecraft:spring_open", + "minecraft:spring_water", + "minecraft:spruce", + "minecraft:spruce_checked", + "minecraft:spruce_on_snow", + "minecraft:sulfur_pool", + "minecraft:sulfur_spike", + "minecraft:sulfur_spike_cluster", + "minecraft:super_birch_bees", + "minecraft:super_birch_bees_0002", + "minecraft:tall_mangrove_checked", + "minecraft:trees_badlands", + "minecraft:trees_birch", + "minecraft:trees_birch_and_oak_leaf_litter", + "minecraft:trees_cherry", + "minecraft:trees_flower_forest", + "minecraft:trees_grove", + "minecraft:trees_jungle", + "minecraft:trees_mangrove", + "minecraft:trees_meadow", + "minecraft:trees_old_growth_pine_taiga", + "minecraft:trees_old_growth_spruce_taiga", + "minecraft:trees_plains", + "minecraft:trees_savanna", + "minecraft:trees_snowy", + "minecraft:trees_sparse_jungle", + "minecraft:trees_swamp", + "minecraft:trees_taiga", + "minecraft:trees_water", + "minecraft:trees_windswept_forest", + "minecraft:trees_windswept_hills", + "minecraft:trees_windswept_savanna", + "minecraft:twisting_vines", + "minecraft:underwater_magma", + "minecraft:vines", + "minecraft:void_start_platform", + "minecraft:warm_ocean_vegetation", + "minecraft:warped_forest_vegetation", + "minecraft:warped_fungi", + "minecraft:weeping_vines", + "minecraft:wildflowers_birch_forest", + "minecraft:wildflowers_meadow", + ] + } } diff --git a/pumpkin-data/src/generated/structures.rs b/pumpkin-data/src/generated/structures.rs index 311a11034..84de0f865 100644 --- a/pumpkin-data/src/generated/structures.rs +++ b/pumpkin-data/src/generated/structures.rs @@ -41,6 +41,126 @@ pub enum StructureKeys { TrailRuins, TrialChambers, } +impl StructureKeys { + pub fn from_name(name: &str) -> Option { + let name = name.strip_prefix("minecraft:").unwrap_or(name); + match name { + "ancient_city" => Some(Self::AncientCity), + "bastion_remnant" => Some(Self::BastionRemnant), + "buried_treasure" => Some(Self::BuriedTreasure), + "desert_pyramid" => Some(Self::DesertPyramid), + "end_city" => Some(Self::EndCity), + "fortress" => Some(Self::Fortress), + "igloo" => Some(Self::Igloo), + "jungle_pyramid" => Some(Self::JunglePyramid), + "mansion" => Some(Self::Mansion), + "mineshaft" => Some(Self::Mineshaft), + "mineshaft_mesa" => Some(Self::MineshaftMesa), + "monument" => Some(Self::Monument), + "nether_fossil" => Some(Self::NetherFossil), + "ocean_ruin_cold" => Some(Self::OceanRuinCold), + "ocean_ruin_warm" => Some(Self::OceanRuinWarm), + "pillager_outpost" => Some(Self::PillagerOutpost), + "ruined_portal" => Some(Self::RuinedPortal), + "ruined_portal_desert" => Some(Self::RuinedPortalDesert), + "ruined_portal_jungle" => Some(Self::RuinedPortalJungle), + "ruined_portal_mountain" => Some(Self::RuinedPortalMountain), + "ruined_portal_nether" => Some(Self::RuinedPortalNether), + "ruined_portal_ocean" => Some(Self::RuinedPortalOcean), + "ruined_portal_swamp" => Some(Self::RuinedPortalSwamp), + "shipwreck" => Some(Self::Shipwreck), + "shipwreck_beached" => Some(Self::ShipwreckBeached), + "stronghold" => Some(Self::Stronghold), + "swamp_hut" => Some(Self::SwampHut), + "trail_ruins" => Some(Self::TrailRuins), + "trial_chambers" => Some(Self::TrialChambers), + "village_desert" => Some(Self::VillageDesert), + "village_plains" => Some(Self::VillagePlains), + "village_savanna" => Some(Self::VillageSavanna), + "village_snowy" => Some(Self::VillageSnowy), + "village_taiga" => Some(Self::VillageTaiga), + _ => None, + } + } + #[must_use] + pub const fn to_name(&self) -> &'static str { + match self { + Self::AncientCity => "minecraft:ancient_city", + Self::BastionRemnant => "minecraft:bastion_remnant", + Self::BuriedTreasure => "minecraft:buried_treasure", + Self::DesertPyramid => "minecraft:desert_pyramid", + Self::EndCity => "minecraft:end_city", + Self::Fortress => "minecraft:fortress", + Self::Igloo => "minecraft:igloo", + Self::JunglePyramid => "minecraft:jungle_pyramid", + Self::Mansion => "minecraft:mansion", + Self::Mineshaft => "minecraft:mineshaft", + Self::MineshaftMesa => "minecraft:mineshaft_mesa", + Self::Monument => "minecraft:monument", + Self::NetherFossil => "minecraft:nether_fossil", + Self::OceanRuinCold => "minecraft:ocean_ruin_cold", + Self::OceanRuinWarm => "minecraft:ocean_ruin_warm", + Self::PillagerOutpost => "minecraft:pillager_outpost", + Self::RuinedPortal => "minecraft:ruined_portal", + Self::RuinedPortalDesert => "minecraft:ruined_portal_desert", + Self::RuinedPortalJungle => "minecraft:ruined_portal_jungle", + Self::RuinedPortalMountain => "minecraft:ruined_portal_mountain", + Self::RuinedPortalNether => "minecraft:ruined_portal_nether", + Self::RuinedPortalOcean => "minecraft:ruined_portal_ocean", + Self::RuinedPortalSwamp => "minecraft:ruined_portal_swamp", + Self::Shipwreck => "minecraft:shipwreck", + Self::ShipwreckBeached => "minecraft:shipwreck_beached", + Self::Stronghold => "minecraft:stronghold", + Self::SwampHut => "minecraft:swamp_hut", + Self::TrailRuins => "minecraft:trail_ruins", + Self::TrialChambers => "minecraft:trial_chambers", + Self::VillageDesert => "minecraft:village_desert", + Self::VillagePlains => "minecraft:village_plains", + Self::VillageSavanna => "minecraft:village_savanna", + Self::VillageSnowy => "minecraft:village_snowy", + Self::VillageTaiga => "minecraft:village_taiga", + } + } + #[must_use] + pub const fn all_names() -> &'static [&'static str] { + &[ + "minecraft:ancient_city", + "minecraft:bastion_remnant", + "minecraft:buried_treasure", + "minecraft:desert_pyramid", + "minecraft:end_city", + "minecraft:fortress", + "minecraft:igloo", + "minecraft:jungle_pyramid", + "minecraft:mansion", + "minecraft:mineshaft", + "minecraft:mineshaft_mesa", + "minecraft:monument", + "minecraft:nether_fossil", + "minecraft:ocean_ruin_cold", + "minecraft:ocean_ruin_warm", + "minecraft:pillager_outpost", + "minecraft:ruined_portal", + "minecraft:ruined_portal_desert", + "minecraft:ruined_portal_jungle", + "minecraft:ruined_portal_mountain", + "minecraft:ruined_portal_nether", + "minecraft:ruined_portal_ocean", + "minecraft:ruined_portal_swamp", + "minecraft:shipwreck", + "minecraft:shipwreck_beached", + "minecraft:stronghold", + "minecraft:swamp_hut", + "minecraft:trail_ruins", + "minecraft:trial_chambers", + "minecraft:village_desert", + "minecraft:village_plains", + "minecraft:village_savanna", + "minecraft:village_snowy", + "minecraft:village_taiga", + ] + } +} pub struct StructureSet { pub placement: StructurePlacement, pub structures: &'static [WeightedEntry], diff --git a/pumpkin-world/build.rs b/pumpkin-world/build.rs index 2a13882a9..d4364a5e3 100644 --- a/pumpkin-world/build.rs +++ b/pumpkin-world/build.rs @@ -38,11 +38,20 @@ fn main() { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let assets_dir = Path::new(&manifest_dir).join("assets/structures"); + let mut all_template_names: Vec = Vec::new(); + let mut all_pool_names: Vec = Vec::new(); if assets_dir.exists() { let mut pools = std::collections::BTreeMap::new(); - process_dir(&assets_dir, "", &mut code, &mut pools); + process_dir( + &assets_dir, + "", + &mut code, + &mut pools, + &mut all_template_names, + ); for (pool_id, elements) in pools { + all_pool_names.push(pool_id.clone()); let _ = writeln!( pool_code, " \"minecraft:{pool_id}\" | \"{pool_id}\" => Some(&[" @@ -57,6 +66,24 @@ fn main() { code.push_str(" _ => None,\n"); code.push_str(" }\n}\n"); + // Generate a function returning all available template names (for tab-completion) + code.push_str( + "#[must_use]\n#[allow(clippy::too_many_lines, clippy::large_stack_arrays)]\npub const fn _generated_all_template_names() -> &'static [&'static str] {\n &[\n", + ); + for name in &all_template_names { + let _ = writeln!(code, " \"{name}\","); + } + code.push_str(" ]\n}\n"); + + // Generate a function returning all available pool names (for tab-completion) + code.push_str( + "#[must_use]\n#[allow(clippy::too_many_lines, clippy::large_stack_arrays)]\npub const fn _generated_all_pool_names() -> &'static [&'static str] {\n &[\n", + ); + for name in &all_pool_names { + let _ = writeln!(code, " \"{name}\","); + } + code.push_str(" ]\n}\n"); + pool_code.push_str(" _ => None,\n"); pool_code.push_str(" }\n}\n"); @@ -65,11 +92,13 @@ fn main() { &worldgen_dir.join("template_pool"), "", &mut template_pool_json_code, + &mut all_pool_names, ); process_json_dir( &worldgen_dir.join("processor_list"), "", &mut processor_list_json_code, + &mut Vec::new(), ); template_pool_json_code.push_str(" _ => None,\n"); template_pool_json_code.push_str(" }\n}\n"); @@ -90,6 +119,7 @@ fn process_dir( prefix: &str, code: &mut String, pools: &mut std::collections::BTreeMap>, + names: &mut Vec, ) { for entry in fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); @@ -102,7 +132,7 @@ fn process_dir( } else { format!("{prefix}/{name}") }; - process_dir(&path, &new_prefix, code, pools); + process_dir(&path, &new_prefix, code, pools, names); } else if path .extension() .and_then(|s| s.to_str()) @@ -121,6 +151,7 @@ fn process_dir( template_name = template_name, abs = abs_path.display() ); + names.push(template_name.clone()); if !prefix.is_empty() { pools @@ -132,7 +163,7 @@ fn process_dir( } } -fn process_json_dir(dir: &Path, prefix: &str, code: &mut String) { +fn process_json_dir(dir: &Path, prefix: &str, code: &mut String, names: &mut Vec) { if !dir.exists() { return; } @@ -152,13 +183,14 @@ fn process_json_dir(dir: &Path, prefix: &str, code: &mut String) { } else { format!("{prefix}/{name}") }; - process_json_dir(&path, &new_prefix, code); + process_json_dir(&path, &new_prefix, code, names); } else if let Some(stem) = name.strip_suffix(".json") { let id = if prefix.is_empty() { stem.to_string() } else { format!("{prefix}/{stem}") }; + names.push(id.clone()); let abs_path = path.canonicalize().unwrap(); let _ = writeln!( code, diff --git a/pumpkin-world/src/generation/feature/mod.rs b/pumpkin-world/src/generation/feature/mod.rs index 12a773f8a..81cb495c5 100644 --- a/pumpkin-world/src/generation/feature/mod.rs +++ b/pumpkin-world/src/generation/feature/mod.rs @@ -1,6 +1,8 @@ -mod configured_features; -/// So first we go trough all the placed features and check if we should place a feature somewhere using `placed_features`. -/// then if we want to place a feature we place it using the `configured_features`, there is the logic for how we are going to place the feature +pub mod configured_features; +// So first we go trough all the placed features and check if we should place a feature +// somewhere using `placed_features`. Then if we want to place a feature we place it +// using the `configured_features`, there is the logic for how we are going to place the +// feature. pub mod placed_features; mod features; diff --git a/pumpkin-world/src/generation/feature/placed_features.rs b/pumpkin-world/src/generation/feature/placed_features.rs index 5ee61c4f1..455cfa060 100644 --- a/pumpkin-world/src/generation/feature/placed_features.rs +++ b/pumpkin-world/src/generation/feature/placed_features.rs @@ -550,6 +550,12 @@ pub trait ConditionalPlacementModifier { } // generated code is now placed alongside other codegen outputs -// in `src/generated` so it’s easier to find when upgrading MC versions. +// in `src/generated` so it's easier to find when upgrading MC versions. // the path is relative to this file (up two levels to reach `src`). + +/// Returns all placed feature names for tab-completion in `/place feature`. +#[must_use] +pub const fn all_placed_feature_names() -> &'static [&'static str] { + pumpkin_data::placed_feature::PlacedFeature::all_names() +} include!("../../../../pumpkin-data/src/generated/placed_features_generated.rs"); diff --git a/pumpkin-world/src/generation/mod.rs b/pumpkin-world/src/generation/mod.rs index 35766d674..d3f37fe72 100644 --- a/pumpkin-world/src/generation/mod.rs +++ b/pumpkin-world/src/generation/mod.rs @@ -5,7 +5,7 @@ pub mod blender; mod block_predicate; mod block_state_provider; pub mod carver; -mod feature; +pub mod feature; pub mod generator; pub mod height_limit; pub mod height_provider; diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 5f5a76edd..96eca7dbc 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -57,6 +57,7 @@ use crate::{ use pumpkin_data::tag::get_tag_ids; use pumpkin_nbt::compound::NbtCompound; +use crate::generation::structure::template::BlockPlacer; use crate::tick::{ScheduledTick, TickPriority}; enum ActiveSupplier { @@ -132,7 +133,7 @@ pub struct ProtoChunk { pub(crate) flat_block_map: Box<[BlockStateId]>, pub flat_biome_map: Box<[u8]>, pub flat_surface_height_map: [i16; CHUNK_AREA], - flat_ocean_floor_height_map: [i16; CHUNK_AREA], + pub flat_ocean_floor_height_map: [i16; CHUNK_AREA], pub flat_motion_blocking_height_map: [i16; CHUNK_AREA], pub flat_motion_blocking_no_leaves_height_map: [i16; CHUNK_AREA], structure_starts: FxHashMap, @@ -1514,3 +1515,87 @@ impl BlockAccessor for ProtoChunk { BlockState::from_id_with_block(id) } } + +impl BlockPlacer for ProtoChunk { + fn get_block_state(&self, pos: &Vector3) -> BlockStateId { + self.get_block_state(pos) + } + + fn set_block_state(&mut self, pos: &Vector3, state: &BlockState) { + Self::set_block_state(self, pos.x, pos.y, pos.z, state); + } + + fn add_block_entity(&mut self, nbt: NbtCompound) { + self.add_block_entity(nbt); + } +} + +impl GenerationCache for ProtoChunk { + fn get_center_chunk_mut(&mut self) -> &mut ProtoChunk { + self + } + fn get_center_chunk(&self) -> &ProtoChunk { + self + } + fn get_chunk_mut(&mut self, cx: i32, cz: i32) -> Option<&mut ProtoChunk> { + (cx == self.x && cz == self.z).then_some(self) + } + fn get_chunk(&self, cx: i32, cz: i32) -> Option<&ProtoChunk> { + (cx == self.x && cz == self.z).then_some(self) + } + fn try_get_proto_chunk(&self, cx: i32, cz: i32) -> Option<&ProtoChunk> { + self.get_chunk(cx, cz) + } + fn get_block_state(&self, pos: &Vector3) -> BlockStateId { + Self::get_block_state(self, pos) + } + fn get_fluid_and_fluid_state(&self, _pos: &Vector3) -> (Fluid, FluidState) { + ( + Fluid::EMPTY, + FluidState { + height: 0.0, + level: 0, + is_empty: true, + blast_resistance: 0.0, + block_state_id: BlockStateId::AIR, + is_still: false, + is_source: false, + falling: false, + }, + ) + } + fn set_block_state(&mut self, pos: &Vector3, block_state: &BlockState) { + Self::set_block_state(self, pos.x, pos.y, pos.z, block_state); + } + fn add_block_entity(&mut self, _pos: &Vector3, nbt: NbtCompound) { + self.add_block_entity(nbt); + } + fn top_motion_blocking_block_height_exclusive(&self, x: i32, z: i32) -> i32 { + Self::top_motion_blocking_block_height_exclusive(self, x, z) + } + fn top_motion_blocking_block_no_leaves_height_exclusive(&self, x: i32, z: i32) -> i32 { + Self::top_motion_blocking_block_no_leaves_height_exclusive(self, x, z) + } + fn get_top_y(&self, heightmap: &HeightMap, x: i32, z: i32) -> i32 { + Self::get_top_y(self, heightmap, x, z) + } + fn top_block_height_exclusive(&self, x: i32, z: i32) -> i32 { + Self::top_block_height_exclusive(self, x, z) + } + fn ocean_floor_height_exclusive(&self, x: i32, z: i32) -> i32 { + Self::ocean_floor_height_exclusive(self, x, z) + } + fn is_air(&self, local_pos: &Vector3) -> bool { + self.is_air(local_pos) + } + fn get_biome_for_terrain_gen(&self, x: i32, y: i32, z: i32) -> &'static Biome { + Self::get_biome(self, x, y, z) + } + fn get_blending_data( + &self, + _cx: i32, + _cz: i32, + ) -> Option<&crate::generation::blender::blending_data::BlendingData> { + None + } +} diff --git a/pumpkin-world/src/generation/structure/mod.rs b/pumpkin-world/src/generation/structure/mod.rs index 8f0077760..2db2b01d2 100644 --- a/pumpkin-world/src/generation/structure/mod.rs +++ b/pumpkin-world/src/generation/structure/mod.rs @@ -30,28 +30,17 @@ pub mod shiftable_piece; pub mod structures; pub mod template; +/// Creates a structure position by dispatching to the appropriate generator. +/// Does NOT perform biome validation — callers that require it should add +/// their own check after calling this function. #[must_use] #[allow(clippy::too_many_lines)] -pub fn try_generate_structure( +pub fn generate_structure_position( key: &StructureKeys, structure: &Structure, - seed: i64, - chunk: &ProtoChunk, - sea_level: i32, - height_sampler: Option<&mut dyn crate::generation::structure::structures::HeightSampler>, + context: StructureGeneratorContext<'_>, ) -> Option { - let random = create_chunk_random(seed, chunk.x, chunk.z); - let context = StructureGeneratorContext { - seed, - chunk_x: chunk.x, - chunk_z: chunk.z, - random, - sea_level, - min_y: chunk.bottom_y() as i32, - height_sampler, - structure_key: Some(*key), - }; - let structure_pos = match key { + match key { StructureKeys::BuriedTreasure => { BuriedTreasureGenerator::get_structure_position(&BuriedTreasureGenerator, context) } @@ -133,7 +122,30 @@ pub fn try_generate_structure( }; generator.get_structure_position(context) } + } +} + +#[must_use] +pub fn try_generate_structure( + key: &StructureKeys, + structure: &Structure, + seed: i64, + chunk: &ProtoChunk, + sea_level: i32, + height_sampler: Option<&mut dyn crate::generation::structure::structures::HeightSampler>, +) -> Option { + let random = create_chunk_random(seed, chunk.x, chunk.z); + let context = StructureGeneratorContext { + seed, + chunk_x: chunk.x, + chunk_z: chunk.z, + random, + sea_level, + min_y: chunk.bottom_y() as i32, + height_sampler, + structure_key: Some(*key), }; + let structure_pos = generate_structure_position(key, structure, context); if let Some(pos) = structure_pos { // Get the biome at the structure's starting position. @@ -177,81 +189,7 @@ pub fn lazily_generate_structure( biome_supplier: &dyn BiomeSupplier, multi_noise_sampler: &mut MultiNoiseSampler, ) -> Option { - let structure_pos = match key { - StructureKeys::BuriedTreasure => { - BuriedTreasureGenerator::get_structure_position(&BuriedTreasureGenerator, context) - } - StructureKeys::SwampHut => { - SwampHutGenerator::get_structure_position(&SwampHutGenerator, context) - } - StructureKeys::Stronghold => { - StrongholdGenerator::get_structure_position(&StrongholdGenerator, context) - } - StructureKeys::Fortress => { - NetherFortressGenerator::get_structure_position(&NetherFortressGenerator, context) - } - StructureKeys::NetherFossil => { - NetherFossilGenerator::get_structure_position(&NetherFossilGenerator, context) - } - StructureKeys::Igloo => IglooGenerator::get_structure_position(&IglooGenerator, context), - StructureKeys::DesertPyramid => DesertPyramidGenerator.get_structure_position(context), - StructureKeys::JunglePyramid => JungleTempleGenerator.get_structure_position(context), - StructureKeys::VillagePlains - | StructureKeys::VillageDesert - | StructureKeys::VillageSavanna - | StructureKeys::VillageSnowy - | StructureKeys::VillageTaiga - | StructureKeys::AncientCity - | StructureKeys::BastionRemnant - | StructureKeys::PillagerOutpost - | StructureKeys::TrailRuins - | StructureKeys::TrialChambers => { - 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 *key == StructureKeys::PillagerOutpost { - generator = generator.with_expansion_hack(true); - } - if let Some(start_jigsaw_name) = structure.start_jigsaw_name { - generator = generator.with_start_jigsaw(start_jigsaw_name); - } - generator.get_structure_position(context) - } - StructureKeys::Shipwreck | StructureKeys::ShipwreckBeached => { - let generator = ShipwreckGenerator { - is_beached: *key == StructureKeys::ShipwreckBeached, - }; - generator.get_structure_position(context) - } - StructureKeys::RuinedPortal - | StructureKeys::RuinedPortalDesert - | StructureKeys::RuinedPortalJungle - | StructureKeys::RuinedPortalSwamp - | StructureKeys::RuinedPortalMountain - | StructureKeys::RuinedPortalOcean - | StructureKeys::RuinedPortalNether => { - let generator = RuinedPortalGenerator { variant: *key }; - generator.get_structure_position(context) - } - StructureKeys::OceanRuinCold | StructureKeys::OceanRuinWarm => { - let generator = OceanRuinGenerator { - is_warm: *key == StructureKeys::OceanRuinWarm, - }; - generator.get_structure_position(context) - } - StructureKeys::EndCity => EndCityGenerator.get_structure_position(context), - StructureKeys::Mansion => MansionGenerator.get_structure_position(context), - StructureKeys::Monument => OceanMonumentGenerator.get_structure_position(context), - StructureKeys::Mineshaft | StructureKeys::MineshaftMesa => { - let generator = MineshaftGenerator { - is_mesa: *key == StructureKeys::MineshaftMesa, - }; - generator.get_structure_position(context) - } - }; + let structure_pos = generate_structure_position(key, structure, context); if let Some(pos) = structure_pos { // Get the biome mathematically, bypassing the chunk boundaries entirely! diff --git a/pumpkin-world/src/generation/structure/structures/jigsaw.rs b/pumpkin-world/src/generation/structure/structures/jigsaw.rs index 513ce81fc..c45982ce0 100644 --- a/pumpkin-world/src/generation/structure/structures/jigsaw.rs +++ b/pumpkin-world/src/generation/structure/structures/jigsaw.rs @@ -5,9 +5,11 @@ use crate::generation::structure::structures::{ StructureGenerator, StructureGeneratorContext, StructurePieceBase, StructurePosition, }; use crate::generation::structure::template::{ - BlockMirror, BlockRotation, PaletteEntry, StructureTemplate, + BlockMirror, BlockPlacer, BlockRotation, PaletteEntry, StructureTemplate, }; +use pumpkin_util::math::block_box::BlockBox; use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; use pumpkin_util::random::RandomImpl; use serde::Deserialize; use std::sync::Arc; @@ -542,6 +544,45 @@ impl StructurePieceBase for PoolElementStructurePiece { } } +pub fn place_pool_element_templates( + piece: &PoolElementStructurePiece, + placer: &mut impl BlockPlacer, + chunk_box: Option<&BlockBox>, +) { + let origin = Vector3::new(piece.pos.0.x, piece.pos.0.y, piece.pos.0.z); + + piece + .element + .for_each_template(|_name, processor_list, legacy, template| { + let corner = piece.rotation.rotate_offset( + template.size.x.saturating_sub(1), + template.size.z.saturating_sub(1), + ); + let placement_origin = Vector3::new( + origin.x + corner.0.min(0), + origin.y, + origin.z + corner.1.min(0), + ); + 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( + placer, + &template, + placement_origin, + (0, 0), + piece.rotation, + legacy, + piece.liquid_settings == LiquidSettings::ApplyWaterlog, + processors.as_ref(), + chunk_box, + ); + }); +} + impl PoolElementStructurePiece { pub fn add_junction(&mut self, junction: JigsawJunction) { self.junctions.push(junction); diff --git a/pumpkin-world/src/generation/structure/template/cache.rs b/pumpkin-world/src/generation/structure/template/cache.rs index 594492066..6fb4449d1 100644 --- a/pumpkin-world/src/generation/structure/template/cache.rs +++ b/pumpkin-world/src/generation/structure/template/cache.rs @@ -143,3 +143,26 @@ pub fn global_cache() -> &'static TemplateCache { pub fn get_template(name: &str) -> Option> { global_cache().get(name) } + +/// Returns a list of all available template names that can be loaded. +/// +/// These are derived from the embedded structure files at compile time. +/// Useful for tab-completion in commands. +#[must_use] +#[allow(clippy::used_underscore_items)] +pub const fn all_template_names() -> &'static [&'static str] { + _generated_all_template_names() +} + +/// Returns a list of all available structure names for `/place structure` tab-completion. +#[must_use] +pub const fn all_structure_names() -> &'static [&'static str] { + pumpkin_data::structures::StructureKeys::all_names() +} + +/// Returns a list of all available pool names for `/place jigsaw` tab-completion. +#[must_use] +#[allow(clippy::used_underscore_items)] +pub const fn all_pool_names() -> &'static [&'static str] { + _generated_all_pool_names() +} diff --git a/pumpkin-world/src/generation/structure/template/mod.rs b/pumpkin-world/src/generation/structure/template/mod.rs index 6ec9646b2..3628f19ea 100644 --- a/pumpkin-world/src/generation/structure/template/mod.rs +++ b/pumpkin-world/src/generation/structure/template/mod.rs @@ -30,6 +30,7 @@ pub mod processor; mod structure_template; mod template_piece; +use pumpkin_data::BlockStateId; use pumpkin_data::Mirror; use pumpkin_data::Rotation; use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; @@ -40,14 +41,22 @@ use crate::ProtoChunk; pub use block_state_resolver::BlockStateResolver; pub use cache::{ - TemplateCache, get_pool_elements, get_processor_list_json, get_template, - get_template_pool_json, global_cache, + TemplateCache, all_pool_names, all_structure_names, all_template_names, 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 pumpkin_data::{BlockState, Mirror as BlockMirror, Rotation as BlockRotation}; pub use structure_template::{PaletteEntry, StructureTemplate, TemplateBlock, TemplateEntity}; pub use template_piece::TemplatePiece; +/// Abstraction over block placement, implemented by both [`ProtoChunk`] (worldgen) and +/// [`WorldBlockPlacer`] (live `/place template` command). +pub trait BlockPlacer { + fn get_block_state(&self, pos: &Vector3) -> BlockStateId; + fn set_block_state(&mut self, pos: &Vector3, state: &BlockState); + fn add_block_entity(&mut self, nbt: NbtCompound); +} + /// Places a template at a world origin with an un-rotated XZ offset. /// /// All rotation is handled internally: @@ -60,7 +69,7 @@ pub use template_piece::TemplatePiece; /// `offset` is the un-rotated XZ offset from origin (`x_offset`, `z_offset`) - rotation is applied automatically. #[allow(clippy::too_many_arguments)] pub fn place_template( - chunk: &mut ProtoChunk, + placer: &mut impl BlockPlacer, template: &StructureTemplate, origin: Vector3, offset: (i32, i32), @@ -126,7 +135,7 @@ 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 + && placer.get_block_state(&world_pos).to_block_id() == pumpkin_data::Block::WATER.id && let Some((_, waterlogged)) = placed_entry .properties .iter_mut() @@ -143,7 +152,7 @@ pub fn place_template( // Apply processors let mut should_place = true; for processor in processors { - let Some(processed_state) = processor.process(chunk, world_pos, state) else { + let Some(processed_state) = processor.process(placer, world_pos, state) else { should_place = false; break; }; @@ -157,7 +166,7 @@ pub fn place_template( continue; } - chunk.set_block_state(wx, wy, wz, state); + placer.set_block_state(&Vector3::new(wx, wy, wz), state); // Create block entities for interactive blocks (furnaces, chests, etc.) let block_entity_id = get_block_entity_id(&placed_entry.name); @@ -189,7 +198,7 @@ pub fn place_template( placed_nbt.put_long("LootTableSeed", random.next_i64()); } - chunk.add_block_entity(placed_nbt); + placer.add_block_entity(placed_nbt); } } } diff --git a/pumpkin-world/src/generation/structure/template/processor.rs b/pumpkin-world/src/generation/structure/template/processor.rs index 0af6dd632..087c0fa90 100644 --- a/pumpkin-world/src/generation/structure/template/processor.rs +++ b/pumpkin-world/src/generation/structure/template/processor.rs @@ -6,7 +6,7 @@ use pumpkin_util::{ use serde::Deserialize; use std::sync::{Arc, LazyLock}; -use crate::ProtoChunk; +use super::BlockPlacer; #[derive(Clone)] pub enum StructureProcessor { @@ -83,7 +83,7 @@ impl StructureProcessor { #[must_use] pub fn process( &self, - chunk: &ProtoChunk, + placer: &impl BlockPlacer, pos: Vector3, state: &'static BlockState, ) -> Option<&'static BlockState> { @@ -106,10 +106,10 @@ impl StructureProcessor { .map_or(Some(state), |rule| Some(rule.output_state)) } Self::ProtectedBlocks(blocks) => { - let existing = chunk.get_block_state(&pos).to_block_id(); + let existing = placer.get_block_state(&pos).to_block_id(); (!blocks.contains(existing)).then_some(state) } - Self::Capped { limit: _, delegate } => delegate.process(chunk, pos, state), + Self::Capped { limit: _, delegate } => delegate.process(placer, pos, state), } } } diff --git a/pumpkin/src/command/argument_types/mod.rs b/pumpkin/src/command/argument_types/mod.rs index d066d3549..c983f0b8b 100644 --- a/pumpkin/src/command/argument_types/mod.rs +++ b/pumpkin/src/command/argument_types/mod.rs @@ -230,10 +230,14 @@ pub mod hex_color; pub mod identifier; pub mod nbt; pub mod objective; +pub mod placed_feature; +pub mod pool; pub mod range; pub mod resource_key; pub mod slot; +pub mod structure; pub mod team; pub mod team_color; +pub mod template; pub mod time; pub mod uuid; diff --git a/pumpkin/src/command/argument_types/placed_feature.rs b/pumpkin/src/command/argument_types/placed_feature.rs new file mode 100644 index 000000000..de7eb4165 --- /dev/null +++ b/pumpkin/src/command/argument_types/placed_feature.rs @@ -0,0 +1,49 @@ +use std::pin::Pin; + +use pumpkin_protocol::java::client::play::SuggestionProviders; +use pumpkin_util::identifier::Identifier; + +use crate::command::argument_types::FromStringReader; +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; + +pub struct PlacedFeatureNameArgumentType; + +impl ArgumentType for PlacedFeatureNameArgumentType { + type Item = Identifier; + + fn parse(&self, reader: &mut StringReader) -> Result { + Identifier::from_reader(reader) + } + + fn list_suggestions<'a>( + &'a self, + _context: &'a CommandContext, + builder: SuggestionsBuilder, + ) -> Pin + Send + 'a>> { + let names = pumpkin_world::generation::feature::placed_features::all_placed_feature_names(); + Box::pin(async move { + builder + .filter_and_suggest_iter(names.iter().copied()) + .build() + }) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::ResourceLocation + } + + fn override_suggestion_providers(&self) -> Option { + Some(SuggestionProviders::AskServer) + } + + fn examples(&self) -> Vec { + vec![ + "minecraft:acacia".to_string(), + "minecraft:amethyst_geode".to_string(), + ] + } +} diff --git a/pumpkin/src/command/argument_types/pool.rs b/pumpkin/src/command/argument_types/pool.rs new file mode 100644 index 000000000..d8f6a12dd --- /dev/null +++ b/pumpkin/src/command/argument_types/pool.rs @@ -0,0 +1,45 @@ +use std::pin::Pin; + +use crate::command::argument_types::FromStringReader; +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; +use pumpkin_protocol::java::client::play::SuggestionProviders; +use pumpkin_util::identifier::Identifier; + +pub struct PoolNameArgumentType; + +impl ArgumentType for PoolNameArgumentType { + type Item = Identifier; + + fn parse(&self, reader: &mut StringReader) -> Result { + Identifier::from_reader(reader) + } + + fn list_suggestions<'a>( + &'a self, + _context: &'a CommandContext, + builder: SuggestionsBuilder, + ) -> Pin + Send + 'a>> { + let names = pumpkin_world::generation::structure::template::all_pool_names(); + Box::pin(async move { + builder + .filter_and_suggest_iter(names.iter().map(|n| format!("minecraft:{n}"))) + .build() + }) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::ResourceLocation + } + + fn override_suggestion_providers(&self) -> Option { + Some(SuggestionProviders::AskServer) + } + + fn examples(&self) -> Vec { + vec!["minecraft:village/plains/houses".to_string()] + } +} diff --git a/pumpkin/src/command/argument_types/structure.rs b/pumpkin/src/command/argument_types/structure.rs new file mode 100644 index 000000000..997356a81 --- /dev/null +++ b/pumpkin/src/command/argument_types/structure.rs @@ -0,0 +1,48 @@ +use std::pin::Pin; + +use crate::command::argument_types::FromStringReader; +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; +use pumpkin_protocol::java::client::play::SuggestionProviders; +use pumpkin_util::identifier::Identifier; + +pub struct StructureNameArgumentType; + +impl ArgumentType for StructureNameArgumentType { + type Item = Identifier; + + fn parse(&self, reader: &mut StringReader) -> Result { + Identifier::from_reader(reader) + } + + fn list_suggestions<'a>( + &'a self, + _context: &'a CommandContext, + builder: SuggestionsBuilder, + ) -> Pin + Send + 'a>> { + let names = pumpkin_world::generation::structure::template::all_structure_names(); + Box::pin(async move { + builder + .filter_and_suggest_iter(names.iter().copied()) + .build() + }) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::ResourceLocation + } + + fn override_suggestion_providers(&self) -> Option { + Some(SuggestionProviders::AskServer) + } + + fn examples(&self) -> Vec { + vec![ + "minecraft:village_plains".to_string(), + "minecraft:ancient_city".to_string(), + ] + } +} diff --git a/pumpkin/src/command/argument_types/template.rs b/pumpkin/src/command/argument_types/template.rs new file mode 100644 index 000000000..cc345b175 --- /dev/null +++ b/pumpkin/src/command/argument_types/template.rs @@ -0,0 +1,46 @@ +use std::pin::Pin; + +use crate::command::argument_types::FromStringReader; +use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType}; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::command_syntax_error::CommandSyntaxError; +use crate::command::string_reader::StringReader; +use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; +use pumpkin_protocol::java::client::play::SuggestionProviders; +use pumpkin_util::identifier::Identifier; + +pub struct TemplateNameArgumentType; + +impl ArgumentType for TemplateNameArgumentType { + type Item = String; + + fn parse(&self, reader: &mut StringReader) -> Result { + let identifier = Identifier::from_reader(reader)?; + Ok(identifier.path().to_string()) + } + + fn list_suggestions<'a>( + &'a self, + _context: &'a CommandContext, + builder: SuggestionsBuilder, + ) -> Pin + Send + 'a>> { + let names = pumpkin_world::generation::structure::template::all_template_names(); + Box::pin(async move { + builder + .filter_and_suggest_iter(names.iter().map(|n| format!("minecraft:{n}"))) + .build() + }) + } + + fn client_side_parser(&'_ self) -> JavaClientArgumentType { + JavaClientArgumentType::ResourceLocation + } + + fn override_suggestion_providers(&self) -> Option { + Some(SuggestionProviders::AskServer) + } + + fn examples(&self) -> Vec { + vec!["igloo/top".to_string()] + } +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index dd91953bb..04fb3d1aa 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -42,6 +42,7 @@ mod op; mod pardon; mod pardonip; mod particle; +mod place; mod playsound; mod plugin; mod plugins; @@ -181,6 +182,7 @@ pub async fn default_dispatcher( help::register(&mut dispatcher, registry); kill::register(&mut dispatcher, registry); op::register(&mut dispatcher, registry); + place::register(&mut dispatcher, registry); random::register(&mut dispatcher, registry); list::register(&mut dispatcher, registry); loot::register(&mut dispatcher, registry); diff --git a/pumpkin/src/command/commands/place.rs b/pumpkin/src/command/commands/place.rs new file mode 100644 index 000000000..f9c759fa0 --- /dev/null +++ b/pumpkin/src/command/commands/place.rs @@ -0,0 +1,750 @@ +use pumpkin_data::chunk_gen_settings::GenerationSettings; +use pumpkin_data::placed_feature::PlacedFeature as PlacedFeatureKey; +use pumpkin_data::structures::{Structure, StructureKeys, StructureType}; +use pumpkin_data::translation; +use pumpkin_data::{Block, BlockStateId, Mirror, Rotation}; +use pumpkin_util::identifier::Identifier; +use pumpkin_util::math::block_box::BlockBox; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::random::hash_block_pos; +use pumpkin_util::text::TextComponent; +use pumpkin_world::generation::proto_chunk::ProtoChunk; +use pumpkin_world::world::{BlockAccessor, WorldPortalExt}; + +use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal}; +use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType; +use crate::command::argument_types::core::integer::IntegerArgumentType; +use crate::command::argument_types::identifier::IdentifierArgumentType; +use crate::command::argument_types::placed_feature::PlacedFeatureNameArgumentType; +use crate::command::argument_types::pool::PoolNameArgumentType; +use crate::command::argument_types::structure::StructureNameArgumentType; +use crate::command::argument_types::template::TemplateNameArgumentType; +use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; +use crate::command::node::dispatcher::CommandDispatcher; +use crate::command::node::{CommandExecutor, CommandExecutorResult}; +use crate::world::block_placer::WorldBlockPlacer; +use pumpkin_world::generation::feature::configured_features::CONFIGURED_FEATURES; +use pumpkin_world::generation::feature::placed_features::{Feature, PLACED_FEATURES}; +use pumpkin_world::generation::structure::structures::StructureGeneratorContext; +use pumpkin_world::generation::structure::structures::jigsaw::{ + PoolElementStructurePiece, place_pool_element_templates, +}; +use pumpkin_world::generation::structure::structures::jigsaw_placement::{ + DimensionPadding, JigsawPlacement, LiquidSettings, MaxDistance, PoolAliasLookup, +}; +use pumpkin_world::generation::structure::template::BlockPlacer; + +use pumpkin_util::PermissionLvl; +use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; +use pumpkin_util::random::RandomGenerator; +use pumpkin_util::random::legacy_rand::LegacyRand; + +const DESCRIPTION: &str = "Places a structure template in the world."; +const PERMISSION: &str = "minecraft:command.place"; + +static TEMPLATE_NOT_FOUND: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_PLACE_TEMPLATE_INVALID, + "commands.place.template.invalid", +); +static JIGSAW_FAILED: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_PLACE_JIGSAW_FAILED, + "commands.place.jigsaw.failed", +); +static STRUCTURE_INVALID: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_PLACE_STRUCTURE_INVALID, + "commands.place.structure.invalid", +); +static FEATURE_INVALID: CommandErrorType<1> = CommandErrorType::new( + translation::java::COMMANDS_PLACE_FEATURE_INVALID, + "commands.place.feature.invalid", +); + +const CHUNK_DIM: i32 = 16; + +/// Clamps a world Y into valid chunk bounds, placing the surface one block below the target. +fn ground_y(block_y: i32, chunk_min_y: i32, chunk_height: i32) -> i32 { + (block_y - 1).clamp(chunk_min_y, chunk_min_y + chunk_height - 1) +} + +/// Vanilla-style chunk-seed derivation used by structure pieces and feature generators. +const fn chunk_population_seed(cx: i32, cz: i32, world_seed: u64) -> u64 { + (cx as i64) + .wrapping_mul(341873128712) + .wrapping_add((cz as i64).wrapping_mul(132897987541)) + .wrapping_add(world_seed as i64) as u64 +} + +/// Minimal `WorldPortalExt` implementation for synthetic chunk placement. +struct CommandBlockRegistry; + +impl WorldPortalExt for CommandBlockRegistry { + fn can_place_at( + &self, + _block: &pumpkin_data::Block, + _state: &pumpkin_data::BlockState, + _block_accessor: &dyn BlockAccessor, + _block_pos: &BlockPos, + ) -> bool { + true + } + + fn mirror( + &self, + block: &pumpkin_data::Block, + state_id: BlockStateId, + mirror: Mirror, + ) -> &'static pumpkin_data::BlockState { + block.mirror(state_id, mirror) + } + + fn rotate( + &self, + block: &pumpkin_data::Block, + state_id: BlockStateId, + rotation: Rotation, + ) -> &'static pumpkin_data::BlockState { + block.rotate(state_id, rotation) + } + + fn spawn_mobs_for_chunk_generation( + &self, + _cache: &mut dyn pumpkin_world::generation::proto_chunk::GenerationCache, + _biome: &'static pumpkin_data::biome::Biome, + _chunk_x: i32, + _chunk_z: i32, + ) { + } +} + +struct PlaceTemplateExecutor; + +impl CommandExecutor for PlaceTemplateExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let template_name = context.get_argument::("template")?.clone(); + + let block_pos = + BlockPosArgumentType::get_block_pos(context, "pos").unwrap_or_else(|_| { + let p = context.source.position; + BlockPos::new(p.x as i32, p.y as i32, p.z as i32) + }); + + let template_name = template_name + .strip_prefix("minecraft:") + .unwrap_or(&template_name) + .to_string(); + let Some(template) = + pumpkin_world::generation::structure::template::get_template(&template_name) + else { + return Err(TEMPLATE_NOT_FOUND + .create_without_context(TextComponent::text(template_name.clone()))); + }; + + let mut placer = WorldBlockPlacer::new(context.world()); + pumpkin_world::generation::structure::template::place_template( + &mut placer, + &template, + block_pos.0, + (0, 0), + Rotation::None, + false, + false, + &[], + None, + ); + + placer.finalize().await; + context + .world() + .queue_block_updates(&placer.changed_positions) + .await; + context.world().flush_block_updates().await; + + context + .source + .send_feedback( + TextComponent::translate( + translation::java::COMMANDS_PLACE_TEMPLATE_SUCCESS, + [ + TextComponent::text(template_name.clone()), + TextComponent::text(block_pos.0.x.to_string()), + TextComponent::text(block_pos.0.y.to_string()), + TextComponent::text(block_pos.0.z.to_string()), + ], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +struct PlaceJigsawExecutor; + +impl CommandExecutor for PlaceJigsawExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let pool = context.get_argument::("pool")?.to_string(); + let target = context.get_argument::("target")?.to_string(); + let max_depth = *context.get_argument::("max_depth")?; + + let block_pos = + BlockPosArgumentType::get_block_pos(context, "pos").unwrap_or_else(|_| { + let p = context.source.position; + BlockPos::new(p.x as i32, p.y as i32, p.z as i32) + }); + + let (piece_count, placer) = { + let seed = hash_block_pos(block_pos.0.x, block_pos.0.y, block_pos.0.z) as u64; + let random = RandomGenerator::Legacy(LegacyRand::from_seed(seed)); + let world_gen = &context.world().level.world_gen; + let settings = GenerationSettings::from_dimension(world_gen.dimension()); + let mut structure_context = StructureGeneratorContext { + seed: seed as i64, + chunk_x: 0, + chunk_z: 0, + random, + sea_level: settings.sea_level, + min_y: world_gen.dimension().min_y, + height_sampler: None, + structure_key: None, + }; + + let position = JigsawPlacement::add_pieces( + &mut structure_context, + &pool, + Some(&target), + max_depth, + block_pos, + false, + false, + &MaxDistance::new(128), + &DimensionPadding::ZERO, + LiquidSettings::ApplyWaterlog, + &PoolAliasLookup, + ) + .ok_or_else(|| { + JIGSAW_FAILED.create_without_context(TextComponent::text(pool.clone())) + })?; + + let collector = position.collector.lock().unwrap(); + let piece_count = collector.pieces.len(); + + let mut placer = WorldBlockPlacer::new(context.world()); + for piece in &collector.pieces { + if let Some(jigsaw_piece) = + piece.as_any().downcast_ref::() + { + place_pool_element_templates(jigsaw_piece, &mut placer, None); + } + } + + (piece_count, placer) + }; + + placer.finalize().await; + context + .world() + .queue_block_updates(&placer.changed_positions) + .await; + context.world().flush_block_updates().await; + + context + .source + .send_feedback( + TextComponent::translate( + translation::java::COMMANDS_PLACE_JIGSAW_SUCCESS, + [ + TextComponent::text(piece_count.to_string()), + TextComponent::text(block_pos.0.x.to_string()), + TextComponent::text(block_pos.0.y.to_string()), + TextComponent::text(block_pos.0.z.to_string()), + ], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +struct PlaceStructureExecutor; + +#[allow(clippy::too_many_lines)] +impl CommandExecutor for PlaceStructureExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let structure_id = context.get_argument::("structure")?; + let structure_name = structure_id.to_string(); + + let key = StructureKeys::from_name(&structure_name).ok_or_else(|| { + STRUCTURE_INVALID + .create_without_context(TextComponent::text(structure_name.clone())) + })?; + + let structure = Structure::get(&key); + + let block_pos = + BlockPosArgumentType::get_block_pos(context, "pos").unwrap_or_else(|_| { + let p = context.source.position; + BlockPos::new(p.x as i32, p.y as i32, p.z as i32) + }); + + let seed = hash_block_pos(block_pos.0.x, block_pos.0.y, block_pos.0.z) as u64; + + let (_piece_count, placer) = { + let world_gen = context.world().level.world_gen.clone(); + let settings = GenerationSettings::from_dimension(world_gen.dimension()); + + if structure.structure_type == StructureType::Jigsaw { + let pool = structure.start_pool.ok_or_else(|| { + STRUCTURE_INVALID + .create_without_context(TextComponent::text(structure_name.clone())) + })?; + let size = structure.size.ok_or_else(|| { + STRUCTURE_INVALID + .create_without_context(TextComponent::text(structure_name.clone())) + })?; + + let random = RandomGenerator::Legacy(LegacyRand::from_seed(seed)); + + let position = JigsawPlacement::add_pieces( + &mut StructureGeneratorContext { + seed: seed as i64, + chunk_x: block_pos.0.x >> 4, + chunk_z: block_pos.0.z >> 4, + random, + sea_level: settings.sea_level, + min_y: world_gen.dimension().min_y, + height_sampler: None, + structure_key: Some(key), + }, + pool, + structure.start_jigsaw_name, + size, + block_pos, + structure.use_expansion_hack.unwrap_or(false), + structure.project_start_to_heightmap.is_some(), + &MaxDistance::new(structure.max_distance_from_center.unwrap_or(128)), + &DimensionPadding::ZERO, + LiquidSettings::ApplyWaterlog, + &PoolAliasLookup, + ) + .ok_or_else(|| { + JIGSAW_FAILED + .create_without_context(TextComponent::text(structure_name.clone())) + })?; + + let collector = position.collector.lock().unwrap(); + let piece_count = collector.pieces.len(); + + let mut placer = WorldBlockPlacer::new(context.world()); + for piece in &collector.pieces { + if let Some(jigsaw_piece) = + piece.as_any().downcast_ref::() + { + place_pool_element_templates(jigsaw_piece, &mut placer, None); + } + } + + (piece_count, placer) + } else { + let random = RandomGenerator::Legacy(LegacyRand::from_seed(seed)); + + let position = + pumpkin_world::generation::structure::generate_structure_position( + &key, + structure, + StructureGeneratorContext { + seed: seed as i64, + chunk_x: block_pos.0.x >> 4, + chunk_z: block_pos.0.z >> 4, + random, + sea_level: settings.sea_level, + min_y: world_gen.dimension().min_y, + height_sampler: None, + structure_key: Some(key), + }, + ) + .ok_or_else(|| { + STRUCTURE_INVALID + .create_without_context(TextComponent::text(structure_name.clone())) + })?; + + let mut collector = position.collector.lock().unwrap(); + let piece_count = collector.pieces.len(); + + let mut placer = WorldBlockPlacer::new(context.world()); + + for piece in &collector.pieces { + if let Some(jigsaw_piece) = + piece.as_any().downcast_ref::() + { + place_pool_element_templates(jigsaw_piece, &mut placer, None); + } + } + + let has_non_jigsaw = collector.pieces.iter().any(|p| { + p.as_any() + .downcast_ref::() + .is_none() + }); + + if has_non_jigsaw { + let reg = CommandBlockRegistry; + + let mut min_x = i32::MAX; + let mut min_z = i32::MAX; + let mut max_x = i32::MIN; + let mut max_z = i32::MIN; + + for piece in &collector.pieces { + if piece + .as_any() + .downcast_ref::() + .is_none() + { + let bb = piece.bounding_box(); + min_x = min_x.min(bb.min.x); + min_z = min_z.min(bb.min.z); + max_x = max_x.max(bb.max.x); + max_z = max_z.max(bb.max.z); + } + } + + let start_cx = min_x >> 4; + let end_cx = max_x >> 4; + let start_cz = min_z >> 4; + let end_cz = max_z >> 4; + + for cx in start_cx..=end_cx { + for cz in start_cz..=end_cz { + let mut chunk = ProtoChunk::new(cx, cz, &world_gen); + let chunk_min_y = chunk.bottom_y() as i32; + let chunk_height = chunk.height() as i32; + let surface_y = ground_y(block_pos.0.y, chunk_min_y, chunk_height); + + // Seed heightmaps and fill below-surface with stone so + // pieces that carve through solid terrain have material + let ground = surface_y as i16; + chunk.flat_surface_height_map = [ground; 256]; + chunk.flat_ocean_floor_height_map = [ground; 256]; + chunk.flat_motion_blocking_height_map = [ground; 256]; + chunk.flat_motion_blocking_no_leaves_height_map = [ground; 256]; + + for x in 0..CHUNK_DIM { + for z in 0..CHUNK_DIM { + for y in chunk_min_y..surface_y { + chunk.set_block_state( + x, + y, + z, + Block::STONE.default_state, + ); + } + chunk.set_block_state( + x, + surface_y, + z, + Block::GRASS_BLOCK.default_state, + ); + } + } + + let chunk_box = BlockBox::new( + cx << 4, + chunk_min_y, + cz << 4, + (cx << 4) + CHUNK_DIM - 1, + chunk_min_y + chunk_height - 1, + (cz << 4) + CHUNK_DIM - 1, + ); + + let snapshot = + snapshot_blocks(&chunk, cx, cz, chunk_min_y, chunk_height); + + let mut rng = RandomGenerator::Legacy(LegacyRand::from_seed( + chunk_population_seed(cx, cz, seed), + )); + + for piece in &mut collector.pieces { + if piece.bounding_box().intersects(&chunk_box) { + piece.place( + &mut chunk, + ®, + &mut rng, + seed as i64, + &chunk_box, + ); + } + } + + apply_delta( + &chunk, + &snapshot, + cx, + cz, + chunk_min_y, + chunk_height, + &mut placer, + ); + + for nbt in chunk.pending_block_entities.drain(..) { + placer.block_entity_nbts.push(nbt); + } + } + } + } + + (piece_count, placer) + } + }; + + placer.finalize().await; + context + .world() + .queue_block_updates(&placer.changed_positions) + .await; + context.world().flush_block_updates().await; + + context + .source + .send_feedback( + TextComponent::translate( + translation::java::COMMANDS_PLACE_STRUCTURE_SUCCESS, + [ + TextComponent::text(structure_name), + TextComponent::text(block_pos.0.x.to_string()), + TextComponent::text(block_pos.0.y.to_string()), + TextComponent::text(block_pos.0.z.to_string()), + ], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +struct PlaceFeatureExecutor; + +impl CommandExecutor for PlaceFeatureExecutor { + fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> { + Box::pin(async move { + let feature_id = context.get_argument::("feature")?; + let feature_name = feature_id.to_string(); + + let key = PlacedFeatureKey::from_name(&feature_name).ok_or_else(|| { + FEATURE_INVALID.create_without_context(TextComponent::text(feature_name.clone())) + })?; + + let placed = PLACED_FEATURES.get(&key).ok_or_else(|| { + FEATURE_INVALID.create_without_context(TextComponent::text(feature_name.clone())) + })?; + + let configured = match &placed.feature { + Feature::Named(name) => CONFIGURED_FEATURES.get(name).ok_or_else(|| { + FEATURE_INVALID + .create_without_context(TextComponent::text(feature_name.clone())) + })?, + Feature::Inlined(f) => f.as_ref(), + }; + + let block_pos = + BlockPosArgumentType::get_block_pos(context, "pos").unwrap_or_else(|_| { + let p = context.source.position; + BlockPos::new(p.x as i32, p.y as i32, p.z as i32) + }); + + let world_gen = context.world().level.world_gen.clone(); + let cx = block_pos.0.x >> 4; + let cz = block_pos.0.z >> 4; + let mut chunk = ProtoChunk::new(cx, cz, &world_gen); + let bottom_y = chunk.bottom_y(); + let height = chunk.height(); + let chunk_min_y = bottom_y as i32; + let chunk_height = height as i32; + let surface_y = ground_y(block_pos.0.y, chunk_min_y, chunk_height); + + let ground = surface_y as i16; + chunk.flat_surface_height_map = [ground; 256]; + chunk.flat_ocean_floor_height_map = [ground; 256]; + chunk.flat_motion_blocking_height_map = [ground; 256]; + chunk.flat_motion_blocking_no_leaves_height_map = [ground; 256]; + + // Feature generation runs against a synthetic solid terrain, not the + // live chunk. Features that depend on existing air, caves, or fluids + // may therefore differ from normal world generation. + for x in 0..CHUNK_DIM { + for z in 0..CHUNK_DIM { + for y in chunk_min_y..surface_y { + chunk.set_block_state(x, y, z, Block::STONE.default_state); + } + chunk.set_block_state(x, surface_y, z, Block::GRASS_BLOCK.default_state); + } + } + + let snapshot = snapshot_blocks(&chunk, cx, cz, chunk_min_y, chunk_height); + + let reg = CommandBlockRegistry; + let seed = hash_block_pos(block_pos.0.x, block_pos.0.y, block_pos.0.z) as u64; + let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(seed)); + + configured.generate( + &mut chunk, + ®, + bottom_y, + height, + key, + &mut random, + block_pos, + ); + + let mut placer = WorldBlockPlacer::new(context.world()); + apply_delta( + &chunk, + &snapshot, + cx, + cz, + chunk_min_y, + chunk_height, + &mut placer, + ); + + for nbt in chunk.pending_block_entities.drain(..) { + placer.block_entity_nbts.push(nbt); + } + + placer.finalize().await; + context + .world() + .queue_block_updates(&placer.changed_positions) + .await; + context.world().flush_block_updates().await; + + context + .source + .send_feedback( + TextComponent::translate( + translation::java::COMMANDS_PLACE_FEATURE_SUCCESS, + [ + TextComponent::text(feature_name), + TextComponent::text(block_pos.0.x.to_string()), + TextComponent::text(block_pos.0.y.to_string()), + TextComponent::text(block_pos.0.z.to_string()), + ], + ), + true, + ) + .await; + + Ok(1) + }) + } +} + +fn snapshot_blocks( + chunk: &ProtoChunk, + cx: i32, + cz: i32, + min_y: i32, + height: i32, +) -> Vec { + let mut snap = Vec::with_capacity((CHUNK_DIM * height * CHUNK_DIM) as usize); + for x in 0..CHUNK_DIM { + for y in min_y..min_y + height { + for z in 0..CHUNK_DIM { + snap.push(chunk.get_block_state(&Vector3::new((cx << 4) + x, y, (cz << 4) + z))); + } + } + } + snap +} + +fn apply_delta( + chunk: &ProtoChunk, + snapshot: &[BlockStateId], + cx: i32, + cz: i32, + min_y: i32, + height: i32, + placer: &mut WorldBlockPlacer<'_>, +) { + // The synthetic chunk is seeded with terrain so structure pieces can carve + // against it. Applying the delta therefore intentionally propagates air + // changes too, which can replace real terrain where a piece carved that + // synthetic stone. + let mut idx = 0usize; + for x in 0..CHUNK_DIM { + for y in min_y..min_y + height { + for z in 0..CHUNK_DIM { + let pos = Vector3::new((cx << 4) + x, y, (cz << 4) + z); + let new_id = chunk.get_block_state(&pos); + if new_id != snapshot[idx] { + placer.set_block_state(&pos, new_id.to_state()); + } + idx += 1; + } + } + } +} + +pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) { + registry.register_permission_or_panic(Permission::new( + PERMISSION, + DESCRIPTION, + PermissionDefault::Op(PermissionLvl::Two), + )); + + dispatcher.register( + command("place", DESCRIPTION) + .requires(PERMISSION) + .then( + literal("template").then( + argument("template", TemplateNameArgumentType) + .executes(PlaceTemplateExecutor) + .then( + argument("pos", BlockPosArgumentType).executes(PlaceTemplateExecutor), + ), + ), + ) + .then( + literal("jigsaw").then( + argument("pool", PoolNameArgumentType).then( + argument("target", IdentifierArgumentType).then( + argument("max_depth", IntegerArgumentType::new(1, 20)) + .executes(PlaceJigsawExecutor) + .then( + argument("pos", BlockPosArgumentType) + .executes(PlaceJigsawExecutor), + ), + ), + ), + ), + ) + .then( + literal("structure").then( + argument("structure", StructureNameArgumentType) + .executes(PlaceStructureExecutor) + .then( + argument("pos", BlockPosArgumentType).executes(PlaceStructureExecutor), + ), + ), + ) + .then( + literal("feature").then( + argument("feature", PlacedFeatureNameArgumentType) + .executes(PlaceFeatureExecutor) + .then(argument("pos", BlockPosArgumentType).executes(PlaceFeatureExecutor)), + ), + ), + ); +} diff --git a/pumpkin/src/world/block_placer.rs b/pumpkin/src/world/block_placer.rs new file mode 100644 index 000000000..47852a52c --- /dev/null +++ b/pumpkin/src/world/block_placer.rs @@ -0,0 +1,51 @@ +use pumpkin_data::{BlockState, BlockStateId}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::generation::structure::template::BlockPlacer; +use pumpkin_world::level::Level; + +use crate::world::World; + +pub struct WorldBlockPlacer<'a> { + world: &'a World, + pub block_entity_nbts: Vec, + pub changed_positions: Vec<(BlockPos, BlockStateId)>, +} + +impl<'a> WorldBlockPlacer<'a> { + #[must_use] + pub const fn new(world: &'a World) -> Self { + Self { + world, + block_entity_nbts: Vec::new(), + changed_positions: Vec::new(), + } + } + + #[allow(clippy::unused_async)] + pub async fn finalize(&self) { + for nbt in &self.block_entity_nbts { + if let Some(block_entity) = crate::block::entities::block_entity_from_nbt(nbt) { + self.world.add_block_entity(block_entity); + } + } + } +} + +impl BlockPlacer for WorldBlockPlacer<'_> { + fn get_block_state(&self, pos: &Vector3) -> BlockStateId { + self.world + .get_block_state_id(&BlockPos::new(pos.x, pos.y, pos.z)) + } + + fn set_block_state(&mut self, pos: &Vector3, state: &BlockState) { + let block_pos = BlockPos::new(pos.x, pos.y, pos.z); + Level::set_block_state(&self.world.level, &block_pos, state.id); + self.changed_positions.push((block_pos, state.id)); + } + + fn add_block_entity(&mut self, nbt: NbtCompound) { + self.block_entity_nbts.push(nbt); + } +} diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 233fec0e4..643b3cd3e 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -143,6 +143,7 @@ use scoreboard::Scoreboard; use time::LevelTime; use tokio::sync::Mutex; +pub mod block_placer; pub mod border; pub mod bossbar; pub mod custom_bossbar; @@ -1038,6 +1039,16 @@ impl World { .insert(position, block_state_id); } + /// Queues block state changes for broadcast to nearby players. + /// + /// Call [`flush_block_updates`](Self::flush_block_updates) afterward to send the packets. + pub async fn queue_block_updates(&self, changes: &[(BlockPos, BlockStateId)]) { + let mut guard = self.unsent_block_changes.lock().await; + for (pos, state_id) in changes { + guard.insert(*pos, *state_id); + } + } + pub async fn flush_block_updates(&self) { let mut block_state_updates_by_chunk_section: HashMap< Vector3,