Merge remote-tracking branch 'origin/master'

This commit is contained in:
Alexander Medvedev 2026-08-20 13:15:58 +02:00
commit af90b14b1c
10 changed files with 1030 additions and 1 deletions

View File

@ -1279,6 +1279,29 @@ impl StructureSet {
Self::VILLAGES, Self::VILLAGES,
Self::WOODLAND_MANSIONS, Self::WOODLAND_MANSIONS,
]; ];
#[doc = r" The registry names of all structure sets, in the same order as [`Self::ALL`]."]
pub const NAMES: &'static [&'static str] = &[
"ancient_cities",
"buried_treasures",
"desert_pyramids",
"end_cities",
"igloos",
"jungle_temples",
"mineshafts",
"nether_complexes",
"nether_fossils",
"ocean_monuments",
"ocean_ruins",
"pillager_outposts",
"ruined_portals",
"shipwrecks",
"strongholds",
"swamp_huts",
"trail_ruins",
"trial_chambers",
"villages",
"woodland_mansions",
];
#[must_use] #[must_use]
pub fn get(name: &str) -> Option<&'static Self> { pub fn get(name: &str) -> Option<&'static Self> {
match name { match name {

View File

@ -0,0 +1,297 @@
use pumpkin_data::chunk::{Biome, BiomeTree, NETHER_BIOME_SOURCE, OVERWORLD_BIOME_SOURCE};
use pumpkin_data::dimension::Dimension;
use pumpkin_util::math::position::BlockPos;
use rustc_hash::FxHashSet;
use crate::biome::{BiomeSupplier, MultiNoiseBiomeSupplier, end::TheEndBiomeSupplier};
use crate::generation::biome_coords;
use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
};
use super::{VanillaGenerator, WorldGenerator};
/// Finds the closest position whose sampled biome id is contained in
/// `targets`, mirroring vanilla's `BiomeSource.findClosestBiome3d`.
///
/// The search spirals outwards from `origin` ring by ring in steps of
/// `horizontal_step` blocks up to `horizontal_radius`, probing each column at
/// y levels spreading out from the origin y in steps of `vertical_step`
/// (staying within the dimension's build height). The first match wins, which
/// approximates the nearest one just like vanilla.
///
/// Returns the block position of the sample point plus the concrete biome
/// found there.
#[must_use]
#[expect(clippy::implicit_hasher)]
pub fn find_closest_biome_3d(
world_gen: &WorldGenerator,
origin: BlockPos,
targets: &FxHashSet<u8>,
horizontal_radius: i32,
horizontal_step: i32,
vertical_step: i32,
) -> Option<(BlockPos, &'static Biome)> {
match world_gen {
WorldGenerator::Flat(flat) => {
// Superflat worlds use a single fixed biome everywhere, matching
// the resolution in `FlatGenerator::step_to_biomes`.
let name = flat.biome.strip_prefix("minecraft:").unwrap_or(&flat.biome);
let biome = Biome::from_name(name).unwrap_or(&Biome::PLAINS);
targets.contains(&biome.id).then_some((origin, biome))
}
WorldGenerator::Noise(generator) => find_in_noise_world(
generator,
origin,
targets,
horizontal_radius,
horizontal_step,
vertical_step,
),
// Plugin generators only expose biomes by generating a whole chunk, so
// there is no sampler to search against.
WorldGenerator::Custom(_) => None,
}
}
fn find_in_noise_world(
generator: &VanillaGenerator,
origin: BlockPos,
targets: &FxHashSet<u8>,
horizontal_radius: i32,
horizontal_step: i32,
vertical_step: i32,
) -> Option<(BlockPos, &'static Biome)> {
// Vanilla intersects the request with `BiomeSource#possibleBiomes` first,
// so asking for a biome that cannot generate in this dimension returns
// immediately instead of scanning the whole search radius.
if targets.is_disjoint(&possible_biomes(&generator.dimension)) {
return None;
}
let supplier: &dyn BiomeSupplier = if generator.dimension == Dimension::THE_END {
&TheEndBiomeSupplier
} else if generator.dimension == Dimension::THE_NETHER {
&MultiNoiseBiomeSupplier::NETHER
} else {
&MultiNoiseBiomeSupplier::OVERWORLD
};
let options = MultiNoiseSamplerBuilderOptions::new(1, 1, 1);
let mut sampler = MultiNoiseSampler::generate(&generator.base_router.multi_noise, &options);
// `level.getMinY() + 1` to `level.getMaxY() + 1` in vanilla.
let min_y = i32::from(generator.settings.shape.min_y) + 1;
let max_y =
i32::from(generator.settings.shape.min_y) + i32::from(generator.settings.shape.height);
let ys = out_from_origin(origin.0.y, min_y, max_y, vertical_step);
let check_column = |x: i32, z: i32, sampler: &mut MultiNoiseSampler| {
let biome_x = biome_coords::from_block(x);
let biome_z = biome_coords::from_block(z);
for &y in &ys {
let biome = supplier.biome(biome_x, biome_coords::from_block(y), biome_z, sampler);
if targets.contains(&biome.id) {
return Some((BlockPos::new(x, y, z), biome));
}
}
None
};
let rings = horizontal_radius / horizontal_step;
for radius in 0..=rings {
if radius == 0 {
if let Some(found) = check_column(origin.0.x, origin.0.z, &mut sampler) {
return Some(found);
}
continue;
}
// Perimeter of the square ring at Chebyshev distance `radius`.
for dx in -radius..=radius {
for dz in [-radius, radius] {
let x = origin.0.x + dx * horizontal_step;
let z = origin.0.z + dz * horizontal_step;
if let Some(found) = check_column(x, z, &mut sampler) {
return Some(found);
}
}
}
for dz in (1 - radius)..radius {
for dx in [-radius, radius] {
let x = origin.0.x + dx * horizontal_step;
let z = origin.0.z + dz * horizontal_step;
if let Some(found) = check_column(x, z, &mut sampler) {
return Some(found);
}
}
}
}
None
}
/// The ids of every biome the given dimension's biome source can produce.
#[must_use]
pub fn possible_biomes(dimension: &Dimension) -> FxHashSet<u8> {
let mut out = FxHashSet::default();
if *dimension == Dimension::THE_END {
// Matches the fixed set in `TheEndBiomeSupplier`.
for biome in [
&Biome::THE_END,
&Biome::END_HIGHLANDS,
&Biome::END_MIDLANDS,
&Biome::SMALL_END_ISLANDS,
&Biome::END_BARRENS,
] {
out.insert(biome.id);
}
} else if *dimension == Dimension::THE_NETHER {
collect_tree_biomes(&NETHER_BIOME_SOURCE, &mut out);
} else {
collect_tree_biomes(&OVERWORLD_BIOME_SOURCE, &mut out);
}
out
}
fn collect_tree_biomes(tree: &'static BiomeTree, out: &mut FxHashSet<u8>) {
match tree {
BiomeTree::Leaf { biome, .. } => {
out.insert(biome.id);
}
BiomeTree::Branch { nodes, .. } => {
for node in *nodes {
collect_tree_biomes(node, out);
}
}
}
}
/// Y levels to probe, ordered outwards from `origin` (upwards first) and
/// clamped to `[min, max]`, mirroring vanilla's `Mth.outFromOrigin`.
fn out_from_origin(origin: i32, min: i32, max: i32, step: i32) -> Vec<i32> {
let start = origin.clamp(min, max);
let mut ys = vec![start];
let mut distance = step;
loop {
let up = start + distance;
let down = start - distance;
if up > max && down < min {
break;
}
if up <= max {
ys.push(up);
}
if down >= min {
ys.push(down);
}
distance += step;
}
ys
}
#[cfg(test)]
mod test {
use pumpkin_data::chunk::Biome;
use pumpkin_data::dimension::Dimension;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::world_seed::Seed;
use rustc_hash::FxHashSet;
use super::super::flat::FlatGenerator;
use super::super::{GeneratorInit, VanillaGenerator, WorldGenerator};
use super::{find_closest_biome_3d, out_from_origin};
fn targets(biomes: &[&Biome]) -> FxHashSet<u8> {
biomes.iter().map(|biome| biome.id).collect()
}
#[test]
fn y_levels_spread_outwards() {
assert_eq!(
out_from_origin(64, -63, 320, 64),
vec![64, 128, 0, 192, 256, 320]
);
// Origin outside the bounds gets clamped first.
assert_eq!(out_from_origin(-500, -63, 320, 200), vec![-63, 137]);
}
#[test]
fn finds_known_biome() {
// Seed 13579 has a desert around block (-96, 4, 32); see
// `biome::test::biome_desert`.
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
Seed(13579),
Dimension::OVERWORLD,
)));
let (pos, biome) = find_closest_biome_3d(
&world_gen,
BlockPos::new(-96, 4, 32),
&targets(&[&Biome::DESERT]),
6400,
32,
64,
)
.expect("a desert should be within range");
assert_eq!(biome.id, Biome::DESERT.id);
assert_eq!(pos, BlockPos::new(-96, 4, 32));
}
#[test]
fn short_circuits_impossible_dimension_biomes() {
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
Seed(13579),
Dimension::OVERWORLD,
)));
// A nether biome can never generate in the overworld; this must
// return without scanning the whole search radius.
assert!(
find_closest_biome_3d(
&world_gen,
BlockPos::new(0, 64, 0),
&targets(&[&Biome::CRIMSON_FOREST]),
6400,
32,
64,
)
.is_none()
);
}
#[test]
fn flat_world_has_a_single_fixed_biome() {
let world_gen = WorldGenerator::Flat(FlatGenerator::new(
Seed(0),
Dimension::OVERWORLD,
Vec::new(),
"minecraft:plains".to_string(),
));
let origin = BlockPos::new(17, 64, -3);
let (pos, biome) = find_closest_biome_3d(
&world_gen,
origin,
&targets(&[&Biome::PLAINS]),
6400,
32,
64,
)
.expect("the flat biome is everywhere");
assert_eq!(biome.id, Biome::PLAINS.id);
assert_eq!(pos, origin);
assert!(
find_closest_biome_3d(
&world_gen,
origin,
&targets(&[&Biome::DESERT]),
6400,
32,
64
)
.is_none()
);
}
}

View File

@ -9,6 +9,7 @@ use super::noise::router::proto_noise_router::ProtoNoiseRouters;
use crate::generation::proto_chunk::TerrainCache; use crate::generation::proto_chunk::TerrainCache;
use crate::generation::{GlobalRandomConfig, Seed}; use crate::generation::{GlobalRandomConfig, Seed};
pub mod biome_finder;
pub mod structure_finder; pub mod structure_finder;
pub trait GeneratorInit { pub trait GeneratorInit {

View File

@ -532,6 +532,51 @@ impl PoiStorage {
results results
} }
/// Finds the closest POI whose type matches `matches`, considering
/// entries within `radius` blocks of `center` on the x/z axes (like
/// vanilla's `PoiManager.findClosestWithType`: a chebyshev square gather
/// followed by picking the smallest 3D squared distance).
///
/// Returns the entry's position together with its type.
pub fn find_closest_matching(
&mut self,
center: BlockPos,
radius: i32,
matches: impl Fn(&str) -> bool,
) -> Option<(BlockPos, String)> {
let min_rx = ((center.0.x - radius) >> 4) >> 5;
let max_rx = ((center.0.x + radius) >> 4) >> 5;
let min_rz = ((center.0.z - radius) >> 4) >> 5;
let max_rz = ((center.0.z + radius) >> 4) >> 5;
let mut best: Option<(BlockPos, String, i64)> = None;
for rx in min_rx..=max_rx {
for rz in min_rz..=max_rz {
let region = self.get_or_load_region(rx, rz);
for entry in region.get_all() {
if (entry.x - center.0.x).abs() > radius
|| (entry.z - center.0.z).abs() > radius
|| !matches(&entry.poi_type)
{
continue;
}
let dx = i64::from(entry.x - center.0.x);
let dy = i64::from(entry.y - center.0.y);
let dz = i64::from(entry.z - center.0.z);
let distance_sq = dx * dx + dy * dy + dz * dz;
if best.as_ref().is_none_or(|(_, _, d)| distance_sq < *d) {
best = Some((entry.pos(), entry.poi_type.clone(), distance_sq));
}
}
}
}
best.map(|(pos, poi_type, _)| (pos, poi_type))
}
pub fn save_all(&mut self) -> std::io::Result<()> { pub fn save_all(&mut self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.folder)?; std::fs::create_dir_all(&self.folder)?;
@ -589,6 +634,41 @@ mod tests {
assert_eq!(region.get_all().len(), 1); assert_eq!(region.get_all().len(), 1);
} }
#[test]
fn poi_find_closest_matching() {
let mut storage = PoiStorage::new(std::env::temp_dir().join("pumpkin_poi_closest_test"));
storage.add_portal(BlockPos(Vector3::new(100, 64, 100)));
storage.add_portal(BlockPos(Vector3::new(120, 64, 100)));
storage.add(BlockPos(Vector3::new(101, 64, 100)), "minecraft:home");
let center = BlockPos(Vector3::new(105, 64, 100));
let (pos, poi_type) = storage
.find_closest_matching(center, 256, |t| t == POI_TYPE_NETHER_PORTAL)
.unwrap();
assert_eq!(pos, BlockPos(Vector3::new(100, 64, 100)));
assert_eq!(poi_type, POI_TYPE_NETHER_PORTAL);
// The overall closest one ignores the type filter mismatch above.
let (pos, poi_type) = storage
.find_closest_matching(center, 256, |_| true)
.unwrap();
assert_eq!(pos, BlockPos(Vector3::new(101, 64, 100)));
assert_eq!(poi_type, "minecraft:home");
assert!(
storage
.find_closest_matching(center, 256, |t| t == "minecraft:lodestone")
.is_none()
);
// Out of horizontal range.
assert!(
storage
.find_closest_matching(BlockPos(Vector3::new(1000, 64, 100)), 16, |_| true)
.is_none()
);
}
#[test] #[test]
fn poi_storage_mca() { fn poi_storage_mca() {
let dir = std::env::temp_dir().join("pumpkin_poi_mca_test"); let dir = std::env::temp_dir().join("pumpkin_poi_mca_test");

View File

@ -235,6 +235,7 @@ pub mod pool;
pub mod range; pub mod range;
pub mod resource; pub mod resource;
pub mod resource_key; pub mod resource_key;
pub mod resource_or_tag;
pub mod slot; pub mod slot;
pub mod structure; pub mod structure;
pub mod team; pub mod team;

View File

@ -0,0 +1,210 @@
use std::collections::BTreeSet;
use std::pin::Pin;
use pumpkin_data::structures::StructureSet;
use pumpkin_data::tag::{self, RegistryKey};
use pumpkin_data::translation;
use pumpkin_util::identifier::Identifier;
use pumpkin_util::text::TextComponent;
use pumpkin_world::poi::POI_TYPE_NETHER_PORTAL;
use crate::command::argument_types::FromStringReader;
use crate::command::argument_types::argument_type::{ArgumentType, JavaClientArgumentType};
use crate::command::argument_types::resource_key::BIOME_REGISTRY;
use crate::command::context::command_context::CommandContext;
use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::string_reader::StringReader;
use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder};
pub static STRUCTURE_REGISTRY: Identifier = Identifier::vanilla_static("worldgen/structure");
pub static POI_REGISTRY: Identifier = Identifier::vanilla_static("point_of_interest_type");
pub static ERROR_UNKNOWN_RESOURCE: CommandErrorType<2> = CommandErrorType::new(
translation::java::ARGUMENT_RESOURCE_NOT_FOUND,
translation::java::ARGUMENT_RESOURCE_NOT_FOUND,
);
pub static ERROR_UNKNOWN_TAG: CommandErrorType<2> = CommandErrorType::new(
translation::java::ARGUMENT_RESOURCE_TAG_NOT_FOUND,
translation::java::ARGUMENT_RESOURCE_TAG_NOT_FOUND,
);
/// A parsed reference to either a single registry entry or a `#`-prefixed tag
/// of entries, as produced by [`ResourceOrTagKeyArgument`] and
/// [`ResourceOrTagArgument`].
#[derive(Debug, Clone)]
pub enum ResourceOrTag {
Resource(Identifier),
Tag(Identifier),
}
impl ResourceOrTag {
/// The user-facing form of this reference (vanilla's `asPrintable`):
/// `namespace:path` for entries, `#namespace:path` for tags.
#[must_use]
pub fn printable(&self) -> String {
match self {
Self::Resource(id) => id.to_string(),
Self::Tag(id) => format!("#{id}"),
}
}
fn from_string_reader(reader: &mut StringReader) -> Result<Self, CommandSyntaxError> {
if reader.peek() == Some('#') {
reader.skip();
Ok(Self::Tag(Identifier::from_reader(reader)?))
} else {
Ok(Self::Resource(Identifier::from_reader(reader)?))
}
}
}
/// Registry-aware suggestions shared by both argument types: the known entry
/// ids plus, when tag data exists for the registry, its `#`-prefixed tags.
fn suggest_for_registry(registry: &Identifier, builder: SuggestionsBuilder) -> Suggestions {
let tag_names = |key: RegistryKey| {
tag::get_latest_map(key)
.into_iter()
.flat_map(|map| map.keys().map(|tag| format!("#{tag}")))
};
if *registry == STRUCTURE_REGISTRY {
// The generator models vanilla's structure sets, so those are the
// locatable names. There is no structure tag data to offer.
builder
.filter_and_suggest_iter(
StructureSet::NAMES
.iter()
.map(|name| format!("minecraft:{name}")),
)
.build()
} else if *registry == *BIOME_REGISTRY {
let biomes = pumpkin_data::biome::Biome::ALL
.iter()
.map(|biome| format!("minecraft:{}", biome.registry_id));
builder
.filter_and_suggest_iter(biomes.chain(tag_names(RegistryKey::WorldgenBiome)))
.build()
} else if *registry == POI_REGISTRY {
// There is no generated POI type registry (yet), so offer the types
// known from tag data plus the ones the server actually creates.
let mut names: BTreeSet<String> = tag::get_latest_map(RegistryKey::PointOfInterestType)
.into_iter()
.flat_map(|map| map.values().flat_map(|tag| tag.0.iter()))
.map(|name| format!("minecraft:{name}"))
.collect();
names.insert(POI_TYPE_NETHER_PORTAL.to_string());
builder
.filter_and_suggest_iter(
names
.into_iter()
.chain(tag_names(RegistryKey::PointOfInterestType)),
)
.build()
} else {
Suggestions::empty()
}
}
/// An argument type that parses an entry id or `#`-tag of a registry.
///
/// The value is not validated against the registry, like vanilla's
/// `ResourceOrTagKeyArgument`; resolution (and the matching error) is left to
/// the command.
pub struct ResourceOrTagKeyArgument(pub Identifier);
impl ArgumentType for ResourceOrTagKeyArgument {
type Item = ResourceOrTag;
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> {
ResourceOrTag::from_string_reader(reader)
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send + 'a>> {
Box::pin(async move { suggest_for_registry(&self.0, builder) })
}
fn client_side_parser(&'_ self) -> JavaClientArgumentType {
JavaClientArgumentType::ResourceOrTagKey {
identifier: self.0.clone(),
}
}
fn examples(&self) -> Vec<String> {
examples!("foo", "foo:bar", "#foo")
}
}
/// An argument type that parses and validates an entry id or `#`-tag of a
/// registry.
///
/// Validation happens at parse time where registry data is available, like
/// vanilla's `ResourceOrTagArgument`: it currently covers biome ids and the
/// tags of every registry with generated tag data; ids of registries without
/// generated entry data (such as POI types) are accepted as-is.
pub struct ResourceOrTagArgument(pub Identifier);
impl ArgumentType for ResourceOrTagArgument {
type Item = ResourceOrTag;
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> {
let value = ResourceOrTag::from_string_reader(reader)?;
match &value {
ResourceOrTag::Resource(id) => {
if self.0 == *BIOME_REGISTRY
&& !(id.is_vanilla()
&& pumpkin_data::biome::Biome::from_name(id.path()).is_some())
{
return Err(ERROR_UNKNOWN_RESOURCE.create_without_context(
TextComponent::text(id.to_string()),
TextComponent::text(self.0.to_string()),
));
}
}
ResourceOrTag::Tag(id) => {
let registry_key = if self.0 == *BIOME_REGISTRY {
Some(RegistryKey::WorldgenBiome)
} else if self.0 == POI_REGISTRY {
Some(RegistryKey::PointOfInterestType)
} else {
None
};
if let Some(key) = registry_key
&& tag::get_tag_values(key, &id.to_string()).is_none()
{
return Err(ERROR_UNKNOWN_TAG.create_without_context(
TextComponent::text(id.to_string()),
TextComponent::text(self.0.to_string()),
));
}
}
}
Ok(value)
}
fn list_suggestions<'a>(
&'a self,
_context: &'a CommandContext,
builder: SuggestionsBuilder,
) -> Pin<Box<dyn Future<Output = Suggestions> + Send + 'a>> {
Box::pin(async move { suggest_for_registry(&self.0, builder) })
}
fn client_side_parser(&'_ self) -> JavaClientArgumentType {
JavaClientArgumentType::ResourceOrTag {
identifier: self.0.clone(),
}
}
fn examples(&self) -> Vec<String> {
examples!("foo", "foo:bar", "#foo")
}
}

View File

@ -0,0 +1,408 @@
use std::borrow::Cow;
use pumpkin_data::biome::Biome;
use pumpkin_data::structures::{StructureKeys, StructurePlacementType, StructureSet};
use pumpkin_data::tag::{self, RegistryKey};
use pumpkin_data::translation;
use pumpkin_util::PermissionLvl;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::click::ClickEvent;
use pumpkin_util::text::hover::HoverEvent;
use pumpkin_util::text::{TextComponent, color::NamedColor};
use pumpkin_world::generation::generator::biome_finder::find_closest_biome_3d;
use pumpkin_world::generation::generator::structure_finder::{
find_nearest_structure, find_nearest_structure_start,
};
use rustc_hash::FxHashSet;
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
use crate::command::argument_types::resource_key::BIOME_REGISTRY;
use crate::command::argument_types::resource_or_tag::{
POI_REGISTRY, ResourceOrTag, ResourceOrTagArgument, ResourceOrTagKeyArgument,
STRUCTURE_REGISTRY,
};
use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::{CommandErrorType, LiteralCommandErrorType};
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
const DESCRIPTION: &str = "Locates the closest structure, biome, or point of interest.";
const PERMISSION: &str = "minecraft:command.locate";
const ARG_STRUCTURE: &str = "structure";
const ARG_BIOME: &str = "biome";
const ARG_POI: &str = "poi";
/// The maximum structure search radius in chunk regions, matching vanilla's
/// `findNearestMapStructure` call in `LocateCommand`.
const STRUCTURE_SEARCH_RADIUS: i32 = 100;
/// Biome search parameters from vanilla's `LocateCommand`: a 6400 block
/// radius probed every 32 blocks horizontally and every 64 blocks vertically.
const BIOME_SEARCH_RADIUS: i32 = 6400;
const BIOME_SEARCH_HORIZONTAL_STEP: i32 = 32;
const BIOME_SEARCH_VERTICAL_STEP: i32 = 64;
/// The POI search radius in blocks, matching vanilla's `LocateCommand`.
const POI_SEARCH_RADIUS: i32 = 256;
static STRUCTURE_INVALID_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_LOCATE_STRUCTURE_INVALID,
translation::java::COMMANDS_LOCATE_STRUCTURE_INVALID,
);
static STRUCTURE_NOT_FOUND_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_LOCATE_STRUCTURE_NOT_FOUND,
translation::bedrock::COMMANDS_LOCATE_STRUCTURE_FAIL_NOSTRUCTUREFOUND,
);
static BIOME_NOT_FOUND_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_LOCATE_BIOME_NOT_FOUND,
translation::bedrock::COMMANDS_LOCATE_BIOME_FAIL,
);
static POI_NOT_FOUND_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_LOCATE_POI_NOT_FOUND,
translation::java::COMMANDS_LOCATE_POI_NOT_FOUND,
);
/// Raised if a blocking search task panics or is cancelled instead of
/// running to completion. Purely internal, so it has no translation.
static SEARCH_FAILED_ERROR_TYPE: LiteralCommandErrorType =
LiteralCommandErrorType::new("The locate search failed unexpectedly");
/// Builds the clickable green `[x, ~, z]` (or `[x, y, z]` when `absolute_y`)
/// coordinates component used by vanilla's locate feedback.
fn coordinates_text(pos: &BlockPos, absolute_y: bool) -> TextComponent {
let x = pos.0.x;
let z = pos.0.z;
let y = if absolute_y {
pos.0.y.to_string()
} else {
"~".to_string()
};
TextComponent::translate_cross(
translation::java::CHAT_COORDINATES,
translation::java::CHAT_COORDINATES,
[
TextComponent::text(x.to_string()),
TextComponent::text(y.clone()),
TextComponent::text(z.to_string()),
],
)
.color_named(NamedColor::Green)
.click_event(ClickEvent::SuggestCommand {
command: Cow::from(format!("/tp @s {x} {y} {z}")),
})
.hover_event(HoverEvent::show_text(TextComponent::translate_cross(
translation::java::CHAT_COORDINATES_TOOLTIP,
translation::java::CHAT_COORDINATES_TOOLTIP,
[],
)))
}
/// The first argument of the success messages: the searched id, with the
/// concretely found entry appended for tag searches, like vanilla's
/// `LocateCommand.showLocateResult`.
fn result_name(searched: &ResourceOrTag, found: &str) -> String {
match searched {
ResourceOrTag::Resource(_) => searched.printable(),
ResourceOrTag::Tag(_) => format!("{} ({found})", searched.printable()),
}
}
/// Vanilla reports the horizontal block distance for structures and POIs.
fn horizontal_distance(origin: &BlockPos, target: &BlockPos) -> i32 {
let dx = f64::from(target.0.x - origin.0.x);
let dz = f64::from(target.0.z - origin.0.z);
dx.hypot(dz).floor().max(0.0) as i32
}
/// ... and the full 3D block distance for biomes.
fn absolute_distance(origin: &BlockPos, target: &BlockPos) -> i32 {
let dx = f64::from(target.0.x - origin.0.x);
let dy = f64::from(target.0.y - origin.0.y);
let dz = f64::from(target.0.z - origin.0.z);
(dx * dx + dy * dy + dz * dz).sqrt().floor().max(0.0) as i32
}
async fn send_success(
context: &CommandContext<'_>,
java_key: &'static str,
bedrock_key: &'static str,
name: String,
target: &BlockPos,
absolute_y: bool,
distance: i32,
) {
context
.source
.send_feedback(
TextComponent::translate_cross(
java_key,
bedrock_key,
[
TextComponent::text(name),
coordinates_text(target, absolute_y),
TextComponent::text(distance.to_string()),
],
),
false,
)
.await;
}
struct LocateStructureExecutor;
impl CommandExecutor for LocateStructureExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let searched = context.get_argument::<ResourceOrTag>(ARG_STRUCTURE)?;
// The generator's placement data models vanilla's structure sets,
// so ids resolve against those. There is no structure tag data,
// hence tags cannot name any known structure (yet).
let set = if let ResourceOrTag::Resource(id) = searched
&& id.is_vanilla()
{
StructureSet::get(id.path())
} else {
None
};
let Some(set) = set else {
return Err(STRUCTURE_INVALID_ERROR_TYPE
.create_without_context(TextComponent::text(searched.printable())));
};
let origin = BlockPos::floored_v(context.source.position);
let world = context.source.world();
let seed = world.level.seed.0;
let world_gen = world.level.world_gen.load_full();
// Scanning up to `STRUCTURE_SEARCH_RADIUS` regions of placement
// data is CPU-bound just like the biome spiral, so keep it off
// the async workers too.
let found = tokio::task::spawn_blocking(move || {
match &set.placement.placement_type {
// Strongholds come out of the pre-computed ring cache, which
// already holds positions they really occupy.
StructurePlacementType::ConcentricRings(_) => {
world_gen.global_structure_cache().and_then(|global_cache| {
find_nearest_structure(
origin,
&[&set.placement],
STRUCTURE_SEARCH_RADIUS,
seed as i64,
global_cache,
)
})
}
// Everything else is spread over a grid whose candidate chunks
// are only *possible* sites: the biome at a candidate can still
// reject every structure in the set. Resolving the start makes
// sure the reported position actually holds one.
StructurePlacementType::RandomSpread(_) => {
let targets: Vec<StructureKeys> =
set.structures.iter().map(|entry| entry.structure).collect();
find_nearest_structure_start(
origin,
set,
&targets,
STRUCTURE_SEARCH_RADIUS,
&world_gen,
)
}
}
})
.await
.map_err(|_| SEARCH_FAILED_ERROR_TYPE.create_without_context())?;
let Some(target) = found else {
return Err(STRUCTURE_NOT_FOUND_ERROR_TYPE
.create_without_context(TextComponent::text(searched.printable())));
};
let distance = horizontal_distance(&origin, &target);
send_success(
context,
translation::java::COMMANDS_LOCATE_STRUCTURE_SUCCESS,
translation::bedrock::COMMANDS_LOCATE_STRUCTURE_SUCCESS,
searched.printable(),
&target,
false,
distance,
)
.await;
Ok(distance)
})
}
}
struct LocateBiomeExecutor;
impl CommandExecutor for LocateBiomeExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let searched = context.get_argument::<ResourceOrTag>(ARG_BIOME)?.clone();
let targets: FxHashSet<u8> = match &searched {
ResourceOrTag::Resource(id) => Biome::from_name(id.path())
.map(|biome| biome.id)
.into_iter()
.collect(),
ResourceOrTag::Tag(id) => {
tag::get_tag_values(RegistryKey::WorldgenBiome, &id.to_string())
.into_iter()
.flatten()
.filter_map(|name| Biome::from_name(name))
.map(|biome| biome.id)
.collect()
}
};
let not_found = || {
BIOME_NOT_FOUND_ERROR_TYPE
.create_without_context(TextComponent::text(searched.printable()))
};
if targets.is_empty() {
return Err(not_found());
}
let origin = BlockPos::floored_v(context.source.position);
let world = context.source.world().clone();
let world_gen = world.level.world_gen.load_full();
// The spiral scan can probe hundreds of thousands of noise
// points when the biome is rare, so keep it off the async
// workers.
let found = tokio::task::spawn_blocking(move || {
find_closest_biome_3d(
&world_gen,
origin,
&targets,
BIOME_SEARCH_RADIUS,
BIOME_SEARCH_HORIZONTAL_STEP,
BIOME_SEARCH_VERTICAL_STEP,
)
})
.await
.map_err(|_| SEARCH_FAILED_ERROR_TYPE.create_without_context())?;
let Some((target, biome)) = found else {
return Err(not_found());
};
let distance = absolute_distance(&origin, &target);
send_success(
context,
translation::java::COMMANDS_LOCATE_BIOME_SUCCESS,
translation::bedrock::COMMANDS_LOCATE_BIOME_SUCCESS,
result_name(&searched, &format!("minecraft:{}", biome.registry_id)),
&target,
true,
distance,
)
.await;
Ok(distance)
})
}
}
struct LocatePoiExecutor;
impl CommandExecutor for LocatePoiExecutor {
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
Box::pin(async move {
let searched = context.get_argument::<ResourceOrTag>(ARG_POI)?;
// POI entries store namespaced type ids, tag data uses bare
// vanilla names; normalize everything to `namespace:path`.
let targets: FxHashSet<String> = match searched {
ResourceOrTag::Resource(id) => std::iter::once(id.to_string()).collect(),
ResourceOrTag::Tag(id) => {
tag::get_tag_values(RegistryKey::PointOfInterestType, &id.to_string())
.into_iter()
.flatten()
.map(|name| {
if name.contains(':') {
(*name).to_string()
} else {
format!("minecraft:{name}")
}
})
.collect()
}
};
let origin = BlockPos::floored_v(context.source.position);
let world = context.source.world().clone();
let found = {
let mut poi_storage = world.portal_poi.lock().await;
poi_storage.find_closest_matching(origin, POI_SEARCH_RADIUS, |poi_type| {
targets.contains(poi_type)
})
};
let Some((target, poi_type)) = found else {
return Err(POI_NOT_FOUND_ERROR_TYPE
.create_without_context(TextComponent::text(searched.printable())));
};
let distance = horizontal_distance(&origin, &target);
send_success(
context,
translation::java::COMMANDS_LOCATE_POI_SUCCESS,
translation::java::COMMANDS_LOCATE_POI_SUCCESS,
result_name(searched, &poi_type),
&target,
false,
distance,
)
.await;
Ok(distance)
})
}
}
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
registry.register_permission_or_panic(Permission::new(
PERMISSION,
DESCRIPTION,
PermissionDefault::Op(PermissionLvl::Two),
));
dispatcher.register(
command("locate", DESCRIPTION)
.requires(PERMISSION)
.then(
literal("structure").then(
argument(
ARG_STRUCTURE,
ResourceOrTagKeyArgument(STRUCTURE_REGISTRY.clone()),
)
.executes(LocateStructureExecutor),
),
)
.then(
literal("biome").then(
argument(ARG_BIOME, ResourceOrTagArgument(BIOME_REGISTRY.clone()))
.executes(LocateBiomeExecutor),
),
)
.then(
literal("poi").then(
argument(ARG_POI, ResourceOrTagArgument(POI_REGISTRY.clone()))
.executes(LocatePoiExecutor),
),
),
);
}

View File

@ -37,6 +37,7 @@ mod item;
mod kick; mod kick;
mod kill; mod kill;
mod list; mod list;
mod locate;
mod loot; mod loot;
mod me; mod me;
mod msg; mod msg;
@ -188,6 +189,7 @@ pub fn default_dispatcher(
place::register(&mut dispatcher, registry); place::register(&mut dispatcher, registry);
random::register(&mut dispatcher, registry); random::register(&mut dispatcher, registry);
list::register(&mut dispatcher, registry); list::register(&mut dispatcher, registry);
locate::register(&mut dispatcher, registry);
loot::register(&mut dispatcher, registry); loot::register(&mut dispatcher, registry);
seed::register(&mut dispatcher, registry); seed::register(&mut dispatcher, registry);
saveall::register(&mut dispatcher, registry); saveall::register(&mut dispatcher, registry);

View File

@ -181,7 +181,7 @@ impl SuggestionsBuilder {
fn matches_substr(pattern: &str, input: &str) -> bool { fn matches_substr(pattern: &str, input: &str) -> bool {
let mut current_str = input; let mut current_str = input;
while !current_str.starts_with(pattern) { while !current_str.starts_with(pattern) {
match current_str.find(['.', '_', '/']) { match current_str.find(['.', '_', '/', ':']) {
Some(pos) => current_str = &current_str[(pos + 1)..], Some(pos) => current_str = &current_str[(pos + 1)..],
None => return false, None => return false,
} }

View File

@ -461,6 +461,7 @@ pub fn build() -> TokenStream {
let mut structure_set_const_defs = TokenStream::new(); let mut structure_set_const_defs = TokenStream::new();
let mut structure_set_lookup_arms = TokenStream::new(); let mut structure_set_lookup_arms = TokenStream::new();
let mut all_structure_set_idents = Vec::new(); let mut all_structure_set_idents = Vec::new();
let mut all_structure_set_names = Vec::new();
for (name, structure_set) in &structure_sets_json { for (name, structure_set) in &structure_sets_json {
let stripped_name = name.strip_prefix("minecraft:").unwrap_or(name); let stripped_name = name.strip_prefix("minecraft:").unwrap_or(name);
@ -476,6 +477,7 @@ pub fn build() -> TokenStream {
)); ));
all_structure_set_idents.push(const_name); all_structure_set_idents.push(const_name);
all_structure_set_names.push(stripped_name.to_string());
} }
let structure_all_names_tokens: Vec<TokenStream> = structure_all_names let structure_all_names_tokens: Vec<TokenStream> = structure_all_names
@ -718,6 +720,11 @@ pub fn build() -> TokenStream {
#(Self::#all_structure_set_idents),* #(Self::#all_structure_set_idents),*
]; ];
/// The registry names of all structure sets, in the same order as [`Self::ALL`].
pub const NAMES: &'static [&'static str] = &[
#(#all_structure_set_names),*
];
#[must_use] #[must_use]
pub fn get(name: &str) -> Option<&'static Self> { pub fn get(name: &str) -> Option<&'static Self> {
match name { match name {