Files
Pumpkin/pumpkin-world/build.rs
Jamie Little 75994f3302 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
2026-07-31 11:45:23 +02:00

204 lines
6.7 KiB
Rust

use std::env;
use std::fmt::Write;
use std::fs;
use std::path::Path;
fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("template_embeddings.rs");
let mut code = String::from(
"
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[must_use]
pub fn get_template_bytes(path: &str) -> Option<&'static [u8]> {\n match path {\n",
);
let mut pool_code = String::from(
"
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[must_use]
pub fn get_pool_elements(pool_id: &str) -> Option<&'static [&'static str]> {\n match pool_id {\n",
);
let mut template_pool_json_code = String::from(
"
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[must_use]
pub fn get_template_pool_json(path: &str) -> Option<&'static str> {\n match path {\n",
);
let mut processor_list_json_code = String::from(
"
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[must_use]
pub fn get_processor_list_json(path: &str) -> Option<&'static str> {\n match path {\n",
);
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let assets_dir = Path::new(&manifest_dir).join("assets/structures");
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,
&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(&["
);
for element in elements {
let _ = writeln!(pool_code, " \"{element}\",");
}
pool_code.push_str(" ]),\n");
}
}
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");
let worldgen_dir = Path::new(&manifest_dir).join("assets/worldgen");
process_json_dir(
&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");
processor_list_json_code.push_str(" _ => None,\n");
processor_list_json_code.push_str(" }\n}\n");
fs::write(
&dest_path,
format!("{code}\n{pool_code}\n{template_pool_json_code}\n{processor_list_json_code}"),
)
.unwrap();
println!("cargo:rerun-if-changed=assets/structures");
println!("cargo:rerun-if-changed=assets/worldgen");
}
fn process_dir(
dir: &Path,
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();
let path = entry.path();
let name = entry.file_name().into_string().unwrap();
if path.is_dir() {
let new_prefix = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
process_dir(&path, &new_prefix, code, pools, names);
} else if path
.extension()
.and_then(|s| s.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("nbt"))
{
let stem = path.file_stem().unwrap().to_string_lossy();
let template_name = if prefix.is_empty() {
stem.to_string()
} else {
format!("{prefix}/{stem}")
};
let abs_path = path.canonicalize().unwrap();
let _ = writeln!(
code,
" \"{template_name}\" => Some(include_bytes!(r#\"{abs}\"#)),",
template_name = template_name,
abs = abs_path.display()
);
names.push(template_name.clone());
if !prefix.is_empty() {
pools
.entry(prefix.to_string())
.or_default()
.push(template_name);
}
}
}
}
fn process_json_dir(dir: &Path, prefix: &str, code: &mut String, names: &mut Vec<String>) {
if !dir.exists() {
return;
}
let mut entries = fs::read_dir(dir)
.unwrap()
.map(Result::unwrap)
.collect::<Vec<_>>();
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let name = entry.file_name().into_string().unwrap();
if path.is_dir() {
let new_prefix = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
process_json_dir(&path, &new_prefix, code, 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,
" \"minecraft:{id}\" | \"{id}\" => Some(include_str!(r#\"{abs}\"#)),",
id = id,
abs = abs_path.display()
);
}
}
}