Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
af90b14b1c
@ -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 {
|
||||||
|
|||||||
297
crates/pumpkin-world/src/generation/generator/biome_finder.rs
Normal file
297
crates/pumpkin-world/src/generation/generator/biome_finder.rs
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
210
crates/pumpkin/src/command/argument_types/resource_or_tag.rs
Normal file
210
crates/pumpkin/src/command/argument_types/resource_or_tag.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
408
crates/pumpkin/src/command/commands/locate.rs
Normal file
408
crates/pumpkin/src/command/commands/locate.rs
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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 = ¤t_str[(pos + 1)..],
|
Some(pos) => current_str = ¤t_str[(pos + 1)..],
|
||||||
None => return false,
|
None => return false,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user