mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(command): add /locate structure (#2420)
* feat(command): add /locate structure Implements /locate structure <name> on top of the existing find_nearest_structure infrastructure (already used by eyes of ender), with tab-completion for the known structure sets and vanilla-style clickable [x, ~, z] teleport coordinates plus horizontal distance in the success message. Notes: - Structure names refer to the generator's structure sets (villages, desert_pyramids, ...), optionally prefixed with minecraft:. - Like the ender-eye path, stronghold (concentric-rings) lookups depend on the global structure cache being populated. Closes #2230 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(command): derive locate suggestions from generated StructureSet::NAMES Addresses review feedback: the structure set names are now emitted by pumpkin-codegen alongside StructureSet::ALL instead of being hard coded in the locate command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(command): add /locate biome and /locate poi, use ResourceOrTag arguments Addresses review feedback on the /locate PR: - New ResourceOrTagKeyArgument / ResourceOrTagArgument types mirroring vanilla's argument pair: both parse `id` or `#tag`, the latter also validates against the generated registry/tag data at parse time and raises vanilla's argument.resource(_tag).not_found errors. /locate structure now uses them instead of StringArgument. - /locate biome: vanilla-parity spiral search (6400 block radius, 32 block horizontal / 64 block vertical steps) over the dimension's noise biome source via the new find_closest_biome_3d in pumpkin-world, including the possibleBiomes short-circuit (walked from the BiomeTree) and superflat support. Runs on a blocking thread, reports the 3D distance and absolute-y clickable coordinates, and appends the concretely found biome for tag searches ("#minecraft:is_forest (minecraft:forest)"), all like vanilla. - /locate poi: searches the world's POI storage within 256 blocks via the new PoiStorage::find_closest_matching (chebyshev gather, closest by 3D distance, like PoiManager.findClosestWithType), with tag support from the generated point_of_interest_type tag data. - Suggestion matching now also treats ':' as a word separator so that e.g. "vil" completes to "minecraft:villages". Notes / limitations: - There is no generated structure tag or POI type registry data yet, so structure "#tags" resolve to the invalid-structure error and POI ids are accepted leniently (unknown ones simply report not-found); only nether portal POIs are currently recorded by the server. Closes #2230 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(command): run structure search off the async executor, drop panic on join errors Review follow-ups on the /locate PR: - /locate structure now runs its search on spawn_blocking like the biome search does; scanning up to 100 regions of placement data is just as CPU-bound as the biome spiral. - Blocking-task join errors (panic/cancellation) are mapped to a "search failed" command error instead of panicking the command task via .expect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(command): adapt /locate to upstream API changes Rebasing onto master picked up three signature changes: WorldGenerator gained a Custom variant for plugin generators, BIOME_REGISTRY became a reference, and Level::world_gen is now an ArcSwap. Plugin generators only expose biomes by generating a whole chunk, so /locate biome reports no match for them rather than pretending to search. * fix(command): report structures /locate can actually find /locate structure returned the placement grid's candidate chunk without checking whether a structure grows there. Candidates are only possible sites: the biome at one can reject every structure in the set, so the command happily pointed at empty terrain. Resolve the structure start for random-spread sets instead, over every structure the set contains, which is what explorer maps already do. Strongholds keep using the ring cache, whose positions are real and which the start lookup does not handle. Co-authored-by: Mcxiaocaibug <Mcxiaocaibug@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1279,6 +1279,29 @@ impl StructureSet {
|
||||
Self::VILLAGES,
|
||||
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]
|
||||
pub fn get(name: &str) -> Option<&'static Self> {
|
||||
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::{GlobalRandomConfig, Seed};
|
||||
|
||||
pub mod biome_finder;
|
||||
pub mod structure_finder;
|
||||
|
||||
pub trait GeneratorInit {
|
||||
|
||||
@@ -532,6 +532,51 @@ impl PoiStorage {
|
||||
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<()> {
|
||||
std::fs::create_dir_all(&self.folder)?;
|
||||
|
||||
@@ -589,6 +634,41 @@ mod tests {
|
||||
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]
|
||||
fn poi_storage_mca() {
|
||||
let dir = std::env::temp_dir().join("pumpkin_poi_mca_test");
|
||||
|
||||
@@ -235,6 +235,7 @@ pub mod pool;
|
||||
pub mod range;
|
||||
pub mod resource;
|
||||
pub mod resource_key;
|
||||
pub mod resource_or_tag;
|
||||
pub mod slot;
|
||||
pub mod structure;
|
||||
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 kill;
|
||||
mod list;
|
||||
mod locate;
|
||||
mod loot;
|
||||
mod me;
|
||||
mod msg;
|
||||
@@ -188,6 +189,7 @@ pub fn default_dispatcher(
|
||||
place::register(&mut dispatcher, registry);
|
||||
random::register(&mut dispatcher, registry);
|
||||
list::register(&mut dispatcher, registry);
|
||||
locate::register(&mut dispatcher, registry);
|
||||
loot::register(&mut dispatcher, registry);
|
||||
seed::register(&mut dispatcher, registry);
|
||||
saveall::register(&mut dispatcher, registry);
|
||||
|
||||
@@ -181,7 +181,7 @@ impl SuggestionsBuilder {
|
||||
fn matches_substr(pattern: &str, input: &str) -> bool {
|
||||
let mut current_str = input;
|
||||
while !current_str.starts_with(pattern) {
|
||||
match current_str.find(['.', '_', '/']) {
|
||||
match current_str.find(['.', '_', '/', ':']) {
|
||||
Some(pos) => current_str = ¤t_str[(pos + 1)..],
|
||||
None => return false,
|
||||
}
|
||||
|
||||
@@ -461,6 +461,7 @@ pub fn build() -> TokenStream {
|
||||
let mut structure_set_const_defs = TokenStream::new();
|
||||
let mut structure_set_lookup_arms = TokenStream::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 {
|
||||
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_names.push(stripped_name.to_string());
|
||||
}
|
||||
|
||||
let structure_all_names_tokens: Vec<TokenStream> = structure_all_names
|
||||
@@ -718,6 +720,11 @@ pub fn build() -> TokenStream {
|
||||
#(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]
|
||||
pub fn get(name: &str) -> Option<&'static Self> {
|
||||
match name {
|
||||
|
||||
Reference in New Issue
Block a user