feat: /place command (#2473)

* Add /place template command with BlockPlacer trait and tab-completion support

- Add BlockPlacer trait to abstract block placement over ProtoChunk (worldgen)
  and WorldBlockPlacer (live command), used by place_template()
- Implement BlockPlacer for ProtoChunk (pumpkin-world) and WorldBlockPlacer
  (pumpkin crate)
- Add World::queue_block_updates() to insert into unsent_block_changes
  without triggering full set_block_state callbacks
- Generate _generated_all_template_names() at build time from structure
  assets for use in tab-completion suggestions
- Add TemplateNameArgumentType for the new command system with
  list_suggestions() using all_template_names()
- Implement /place template <template> [pos] in the new command system
  (CommandDispatcher, CommandExecutor, ArgumentBuilder)
- Fix borrow-across-await: clone template name to owned String early

* feat: add /place structure and /place jigsaw commands

Adds /place structure <id> [pos] for all structure types with two placement paths:
- Jigsaw structures (ancient_city, bastion_remnant, etc.): fast path using
  JigsawPlacement::add_pieces directly at the target position
- Non-jigsaw structures (desert_pyramid, end_city, etc.): generate pieces
  via dispatch, place into synthetic ProtoChunks with pre-seeded heightmaps
  and a stone floor so pieces can detect terrain, then delta-apply only
  changed blocks to the world via WorldBlockPlacer

Also adds /place jigsaw <pool> <target> <depth> [pos] for manual jigsaw
template placement.

Architecture:
- generate_structure_position() extracts the shared generator dispatch
  from try_generate_structure / lazily_generate_structure (both now
  delegate to it, eliminating ~150 lines of duplicated match arms)
- place_pool_element_templates() extracted from PoolElementStructurePiece
  for reuse by the command with WorldBlockPlacer
- StructureKeys gains from_name()/to_name()/all_names() via codegen
- StructureNameArgumentType and PoolNameArgumentType for tab-completion
- flat_ocean_floor_height_map made pub in ProtoChunk for heightmap seeding

* feat: add /place feature command

Adds /place feature <feature> [pos] for placing configured features
at a specific position. Resolves the PlacedFeature name via from_name(),
resolves the inner ConfiguredFeature from PLACED_FEATURES /
CONFIGURED_FEATURES, then calls ConfiguredFeature::generate() directly
at the target position -- skipping placement modifiers so the feature
appears exactly where specified.

Also adds /place structure improvements:
- Synthetic chunk terrain fill: stone below surface + grass on top, so
  pieces that carve through solid terrain (stronghold corridors, etc.)
  find material to work with. Snapshot/delta ensures only structure
  blocks reach the world.
- Shared snapshot_blocks() and apply_delta() helpers eliminate ~30 lines
  of duplicated diff logic between structure and feature paths.
- ground_y() and chunk_population_seed() helpers replace magic numbers.
- Structure success message now reports the structure name instead of
  the piece count.

Infrastructure:
- PlacedFeature::all_names() generated via pumpkin-codegen for
  tab-completion suggestions in PlacedFeatureNameArgumentType.
- GenerationCache trait implemented for ProtoChunk (single-chunk
  delegation) so ConfiguredFeature::generate() works without the full
  chunk-generation cache system.
- flat_ocean_floor_height_map made pub in ProtoChunk for heightmap
  seeding.
- configured_features and feature modules made pub for access from
  the command crate.

* fix(command): use world settings for place

* docs(place): clarify synthetic terrain delta behavior
This commit is contained in:
Jamie Little
2026-07-31 10:45:23 +01:00
committed by GitHub
parent 732aa24fe3
commit 75994f3302
23 changed files with 1699 additions and 115 deletions

View File

@@ -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<String> = Vec::new();
let variants: Vec<TokenStream> = 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<TokenStream> = 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),*]
}
}
}
}

View File

@@ -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<String> = 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<TokenStream> = 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<Self> {
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],

View File

@@ -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",
]
}
}

View File

@@ -41,6 +41,126 @@ pub enum StructureKeys {
TrailRuins,
TrialChambers,
}
impl StructureKeys {
pub fn from_name(name: &str) -> Option<Self> {
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],

View File

@@ -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<String> = Vec::new();
let mut all_pool_names: Vec<String> = 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<String, Vec<String>>,
names: &mut Vec<String>,
) {
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<String>) {
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,

View File

@@ -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;

View File

@@ -550,6 +550,12 @@ pub trait ConditionalPlacementModifier {
}
// generated code is now placed alongside other codegen outputs
// in `src/generated` so its 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");

View File

@@ -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;

View File

@@ -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<StructureKeys, StructureInstance>,
@@ -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<i32>) -> BlockStateId {
self.get_block_state(pos)
}
fn set_block_state(&mut self, pos: &Vector3<i32>, 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<i32>) -> BlockStateId {
Self::get_block_state(self, pos)
}
fn get_fluid_and_fluid_state(&self, _pos: &Vector3<i32>) -> (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<i32>, block_state: &BlockState) {
Self::set_block_state(self, pos.x, pos.y, pos.z, block_state);
}
fn add_block_entity(&mut self, _pos: &Vector3<i32>, 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<i32>) -> 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
}
}

View File

@@ -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<StructurePosition> {
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<StructurePosition> {
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<StructurePosition> {
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!

View File

@@ -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);

View File

@@ -143,3 +143,26 @@ pub fn global_cache() -> &'static TemplateCache {
pub fn get_template(name: &str) -> Option<Arc<StructureTemplate>> {
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()
}

View File

@@ -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<i32>) -> BlockStateId;
fn set_block_state(&mut self, pos: &Vector3<i32>, 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<i32>,
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);
}
}
}

View File

@@ -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<i32>,
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),
}
}
}

View File

@@ -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;

View File

@@ -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<Self::Item, CommandSyntaxError> {
Identifier::from_reader(reader)
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + 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<SuggestionProviders> {
Some(SuggestionProviders::AskServer)
}
fn examples(&self) -> Vec<String> {
vec![
"minecraft:acacia".to_string(),
"minecraft:amethyst_geode".to_string(),
]
}
}

View File

@@ -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<Self::Item, CommandSyntaxError> {
Identifier::from_reader(reader)
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + 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<SuggestionProviders> {
Some(SuggestionProviders::AskServer)
}
fn examples(&self) -> Vec<String> {
vec!["minecraft:village/plains/houses".to_string()]
}
}

View File

@@ -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<Self::Item, CommandSyntaxError> {
Identifier::from_reader(reader)
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + 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<SuggestionProviders> {
Some(SuggestionProviders::AskServer)
}
fn examples(&self) -> Vec<String> {
vec![
"minecraft:village_plains".to_string(),
"minecraft:ancient_city".to_string(),
]
}
}

View File

@@ -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<Self::Item, CommandSyntaxError> {
let identifier = Identifier::from_reader(reader)?;
Ok(identifier.path().to_string())
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + 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<SuggestionProviders> {
Some(SuggestionProviders::AskServer)
}
fn examples(&self) -> Vec<String> {
vec!["igloo/top".to_string()]
}
}

View File

@@ -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);

View File

@@ -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::<String>("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::<Identifier>("pool")?.to_string();
let target = context.get_argument::<Identifier>("target")?.to_string();
let max_depth = *context.get_argument::<i32>("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::<PoolElementStructurePiece>()
{
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::<Identifier>("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::<PoolElementStructurePiece>()
{
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::<PoolElementStructurePiece>()
{
place_pool_element_templates(jigsaw_piece, &mut placer, None);
}
}
let has_non_jigsaw = collector.pieces.iter().any(|p| {
p.as_any()
.downcast_ref::<PoolElementStructurePiece>()
.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::<PoolElementStructurePiece>()
.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,
&reg,
&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::<Identifier>("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,
&reg,
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<BlockStateId> {
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)),
),
),
);
}

View File

@@ -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<NbtCompound>,
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<i32>) -> BlockStateId {
self.world
.get_block_state_id(&BlockPos::new(pos.x, pos.y, pos.z))
}
fn set_block_state(&mut self, pos: &Vector3<i32>, 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);
}
}

View File

@@ -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<i32>,