feat: Eye of Ender pointing to Stronghold

This commit is contained in:
Alexander Medvedev 2026-04-08 12:43:43 +02:00
parent 0017b1bf4c
commit eed57ce5fc
6 changed files with 492 additions and 24 deletions

View File

@ -9,6 +9,8 @@ use super::noise::router::proto_noise_router::ProtoNoiseRouters;
use crate::generation::proto_chunk::TerrainCache;
use crate::generation::{GlobalRandomConfig, Seed};
pub mod structure_finder;
pub trait GeneratorInit {
fn new(seed: Seed, dimension: Dimension) -> Self;
}

View File

@ -0,0 +1,170 @@
use pumpkin_data::structures::{
ConcentricRingsStructurePlacement, RandomSpreadStructurePlacement, StructurePlacement,
StructurePlacementType,
};
use pumpkin_util::math::{floor_div, position::BlockPos};
use crate::generation::structure::placement::{
GlobalStructureCache, get_structure_chunk_in_region,
};
/// Block-level position of a found structure plus squared distance from the
/// search origin, used internally to track the running nearest candidate.
#[derive(Debug, Clone)]
pub struct FoundStructure {
pub pos: BlockPos,
pub distance_sq: f64,
}
/// Finds the block position of the nearest structure whose placement is listed
/// in `placements`, within `max_search_radius` chunk-region rings.
///
/// Mirrors the two-pass logic in vanilla's
/// `ChunkGenerator.findNearestMapStructure`:
///
/// 1. **Concentric-rings** placements (strongholds) are resolved in one pass
/// from the pre-computed [`GlobalStructureCache`].
/// 2. **Random-spread** placements are searched ring-by-ring outward, stopping
/// at the first radius that produces any result.
///
/// The best candidate from both passes is returned.
pub fn find_nearest_structure(
origin: BlockPos,
placements: &[&StructurePlacement],
max_search_radius: i32,
world_seed: i64,
global_cache: &GlobalStructureCache,
) -> Option<BlockPos> {
if placements.is_empty() {
return None;
}
let mut nearest: Option<FoundStructure> = None;
// ── Pass 1: Concentric-rings (strongholds) ──────────────────────────────
for p in placements {
if let StructurePlacementType::ConcentricRings(rings) = &p.placement_type
&& let Some(found) = find_nearest_concentric(origin, rings, global_cache)
&& nearest
.as_ref()
.is_none_or(|n| found.distance_sq < n.distance_sq)
{
nearest = Some(found);
}
}
let random_spread: Vec<(&RandomSpreadStructurePlacement, u32)> = placements
.iter()
.filter_map(|p| {
if let StructurePlacementType::RandomSpread(r) = &p.placement_type {
Some((r, p.salt))
} else {
None
}
})
.collect();
if !random_spread.is_empty() {
let chunk_origin_x = origin.0.x >> 4;
let chunk_origin_z = origin.0.z >> 4;
'radius: for radius in 0..=max_search_radius {
for (placement, salt) in &random_spread {
if let Some(found) = find_nearest_random_spread_at_radius(
origin,
chunk_origin_x,
chunk_origin_z,
radius,
world_seed,
placement,
*salt,
) {
if nearest
.as_ref()
.is_none_or(|n| found.distance_sq < n.distance_sq)
{
nearest = Some(found);
}
break 'radius;
}
}
}
}
nearest.map(|f| f.pos)
}
fn find_nearest_concentric(
origin: BlockPos,
// Kept for potential future bounds / distance validation.
_rings: &ConcentricRingsStructurePlacement,
global_cache: &GlobalStructureCache,
) -> Option<FoundStructure> {
let strongholds = global_cache.get_stronghold_chunks();
if strongholds.is_empty() {
return None;
}
let ox = origin.0.x as f64;
let oz = origin.0.z as f64;
strongholds
.iter()
.map(|(cx, cz)| {
// Centre of the chunk in block coords.
let bx = (cx << 4) + 8;
let bz = (cz << 4) + 8;
let dx = bx as f64 - ox;
let dz = bz as f64 - oz;
FoundStructure {
pos: BlockPos::new(bx, 0, bz),
distance_sq: dx * dx + dz * dz,
}
})
.min_by(|a, b| a.distance_sq.partial_cmp(&b.distance_sq).unwrap())
}
fn find_nearest_random_spread_at_radius(
origin: BlockPos,
chunk_origin_x: i32,
chunk_origin_z: i32,
radius: i32,
world_seed: i64,
placement: &RandomSpreadStructurePlacement,
salt: u32,
) -> Option<FoundStructure> {
let spacing = placement.spacing;
let ox = origin.0.x as f64;
let oz = origin.0.z as f64;
let mut best: Option<FoundStructure> = None;
for rx_off in -radius..=radius {
for rz_off in -radius..=radius {
if rx_off.abs() != radius && rz_off.abs() != radius {
continue;
}
let rx = floor_div(chunk_origin_x, spacing) + rx_off;
let rz = floor_div(chunk_origin_z, spacing) + rz_off;
let (struct_cx, struct_cz) =
get_structure_chunk_in_region(placement, world_seed, rx, rz, salt);
let bx = (struct_cx << 4) + 8;
let bz = (struct_cz << 4) + 8;
let dx = bx as f64 - ox;
let dz = bz as f64 - oz;
let dist_sq = dx * dx + dz * dz;
if best.as_ref().is_none_or(|b| dist_sq < b.distance_sq) {
best = Some(FoundStructure {
pos: BlockPos::new(bx, 0, bz),
distance_sq: dist_sq,
});
}
}
}
best
}

View File

@ -32,6 +32,13 @@ impl GlobalStructureCache {
}
}
pub fn get_stronghold_chunks(&self) -> &[(i32, i32)] {
self.stronghold_chunks
.get()
.map(|v| v.as_slice())
.unwrap_or(&[])
}
/// Retrieves the list of chunk coordinates for Concentric Ring structures.
/// If the cache is empty, it calculates the 128 ring positions mathematically.
#[allow(clippy::cast_precision_loss)]

View File

@ -0,0 +1,222 @@
use crate::entity::item::ItemEntity;
use crate::entity::living::LivingEntity;
use crate::entity::player::Player;
use crate::{entity::EntityBaseFuture, server::Server};
use core::f64;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::Sound;
use pumpkin_data::world::WorldEvent;
use pumpkin_data::{damage::DamageType, meta_data_type::MetaDataType, tracked_data::TrackedData};
use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer, java::client::play::Metadata,
};
use pumpkin_util::math::vector3::Vector3;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU32, Ordering},
};
use tokio::sync::Mutex;
use super::{Entity, EntityBase, NBTStorage};
/// Maximum horizontal distance the eye will travel before signalling at an elevated height.
const TOO_FAR_DISTANCE: f64 = 12.0;
/// Height offset used when the target is beyond [`TOO_FAR_DISTANCE`].
const TOO_FAR_SIGNAL_HEIGHT: f64 = 8.0;
/// The eye despawns after this many ticks.
const MAX_LIFE: u32 = 80;
/// Chance (1-in-5) the eye drops its item instead of playing the break effect.
const SURVIVE_CHANCE: u32 = 5;
pub struct EyeOfEnder {
entity: Entity,
item_stack: Mutex<ItemStack>,
target: Mutex<Option<Vector3<f64>>>,
life: AtomicU32,
survive_after_death: AtomicBool,
}
impl EyeOfEnder {
pub fn new(entity: Entity) -> Self {
Self {
entity,
item_stack: Mutex::new(ItemStack::new(1, &Item::ENDER_EYE)),
target: Mutex::new(None),
life: AtomicU32::new(0),
survive_after_death: AtomicBool::new(false),
}
}
/// Aim the eye at `target`, clamping to [`TOO_FAR_DISTANCE`] if necessary,
/// and randomly decide whether it should drop its item on expiry.
pub async fn signal_to(&self, target: Vector3<f64>) {
let pos = self.entity.pos.load();
let delta = target.sub(&pos);
let horizontal_dist = delta.x.hypot(delta.z);
let clamped_target = if horizontal_dist > TOO_FAR_DISTANCE {
Vector3::new(
pos.x + delta.x / horizontal_dist * TOO_FAR_DISTANCE,
pos.y + TOO_FAR_SIGNAL_HEIGHT,
pos.z + delta.z / horizontal_dist * TOO_FAR_DISTANCE,
)
} else {
target
};
*self.target.lock().await = Some(clamped_target);
self.life.store(0, Ordering::Relaxed);
// 4-in-5 chance to survive (drop item); 1-in-5 plays the break effect.
let survive = !rand::random::<u32>().is_multiple_of(SURVIVE_CHANCE);
self.survive_after_death.store(survive, Ordering::Relaxed);
}
/// Compute the next delta-movement so the eye smoothly homes toward `target`.
///
/// Mirrors the vanilla `updateDeltaMovement` logic.
fn compute_new_velocity(
old_velo: Vector3<f64>,
position: Vector3<f64>,
target: Vector3<f64>,
) -> Vector3<f64> {
let horizontal_delta = Vector3::new(target.x - position.x, 0.0, target.z - position.z);
let horizontal_len = horizontal_delta.x.hypot(horizontal_delta.z);
let old_h_speed = old_velo.x.hypot(old_velo.z);
let mut wanted_speed = lerp(0.0025, old_h_speed, horizontal_len);
let mut move_y = old_velo.y;
if horizontal_len < 1.0 {
wanted_speed *= 0.8;
move_y *= 0.8;
}
let wanted_y_dir = if position.y - old_velo.y < target.y {
1.0f64
} else {
-1.0f64
};
let scale = wanted_speed / horizontal_len;
Vector3::new(
horizontal_delta.x * scale,
move_y + (wanted_y_dir - move_y) * 0.015,
horizontal_delta.z * scale,
)
}
}
/// Linear interpolation helper (`start + t * (end - start)`).
#[inline]
fn lerp(t: f64, start: f64, end: f64) -> f64 {
start + t * (end - start)
}
impl NBTStorage for EyeOfEnder {}
impl EntityBase for EyeOfEnder {
fn tick<'a>(
&'a self,
caller: Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.entity;
entity.tick(caller, server).await;
// Advance position by current velocity.
let velocity = entity.velocity.load();
let new_pos = entity.pos.load().add(&velocity);
// Server-side: steer toward target if one is set.
{
let target_guard = self.target.lock().await;
if let Some(target) = *target_guard {
let new_velo = Self::compute_new_velocity(velocity, new_pos, target);
entity.velocity.store(new_velo);
}
}
entity.set_pos(new_pos);
entity.send_pos_rot().await;
// Tick lifetime and handle expiry.
let life = self.life.fetch_add(1, Ordering::Relaxed) + 1;
if life > MAX_LIFE {
entity.play_sound(Sound::EntityEnderEyeDeath).await;
entity.remove().await;
if self.survive_after_death.load(Ordering::Relaxed) {
// Drop the item at the current position.
let item_stack = self.item_stack.lock().await.clone();
let world = entity.world.load();
let entity = Entity::new(world.clone(), new_pos, &EntityType::ITEM);
let item_entity = Arc::new(ItemEntity::new(entity, item_stack).await);
world.spawn_entity(item_entity).await;
} else {
entity
.world
.load()
.sync_world_event(
WorldEvent::ParticlesEyeOfEnderDeath,
new_pos.to_block_pos(),
0,
)
.await;
}
}
})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async {
self.entity
.send_meta_data(&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(self.item_stack.lock().await.clone()),
)])
.await;
})
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
// Eye of Ender is not attackable.
Box::pin(async { false })
}
fn on_player_collision<'a>(&'a self, _player: &'a Arc<Player>) -> EntityBaseFuture<'a, ()> {
// Eye of Ender cannot be picked up.
Box::pin(async {})
}
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn get_item_entity(self: Arc<Self>) -> Option<Arc<ItemEntity>> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
}

View File

@ -11,6 +11,7 @@ use std::{
};
pub mod arrow;
pub mod egg;
pub mod eye_of_ender;
pub mod firework_rocket;
pub mod lingering_potion;
pub mod snowball;

View File

@ -1,15 +1,25 @@
use std::pin::Pin;
use std::sync::Arc;
use crate::{server::Server, world::portal::end::EndPortal};
use crate::entity::Entity;
use crate::entity::projectile::eye_of_ender::EyeOfEnder;
use crate::item::{ItemBehaviour, ItemMetadata};
use crate::server::Server;
use crate::world::World;
use crate::world::portal::end::EndPortal;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::{Block, BlockDirection, item::Item};
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::structures::StructureSet;
use pumpkin_data::world::WorldEvent;
use pumpkin_data::{Block, BlockDirection};
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::generation::generator::structure_finder::find_nearest_structure;
use pumpkin_world::world::BlockFlags;
use crate::item::{ItemBehaviour, ItemMetadata};
use crate::{entity::player::Player, world::World};
use crate::entity::player::Player;
pub struct EnderEyeItem;
@ -22,7 +32,7 @@ impl ItemMetadata for EnderEyeItem {
impl ItemBehaviour for EnderEyeItem {
fn use_on_block<'a>(
&'a self,
_item: &'a mut ItemStack,
item: &'a mut ItemStack,
player: &'a Player,
location: BlockPos,
_face: BlockDirection,
@ -37,17 +47,17 @@ impl ItemBehaviour for EnderEyeItem {
let world = player.world();
let state_id = world.get_block_state_id(&location).await;
let original_props = block.properties(state_id).unwrap().to_props();
let props: Vec<(&str, &str)> = original_props
.iter()
.map(|(key, value)| {
if *key == "eye" {
(*key, "true")
} else {
(*key, *value)
// Skip if the frame already holds an eye.
let props_raw = block.properties(state_id).unwrap().to_props();
if props_raw.iter().any(|(k, v)| *k == "eye" && *v == "true") {
return;
}
})
// Build new state with eye=true.
let props: Vec<(&str, &str)> = props_raw
.iter()
.map(|(k, v)| if *k == "eye" { (*k, "true") } else { (*k, *v) })
.collect();
let new_state_id = block.from_properties(&props).to_state_id(block);
@ -55,7 +65,15 @@ impl ItemBehaviour for EnderEyeItem {
.set_block_state(&location, new_state_id, BlockFlags::empty())
.await;
world
.sync_world_event(WorldEvent::EndPortalFrameFill, location, 0)
.await;
// Try to complete the portal.
EndPortal::get_new_portal(&world, location).await;
// Consume one item.
item.decrement(1);
})
}
@ -66,25 +84,73 @@ impl ItemBehaviour for EnderEyeItem {
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
let world = player.world();
let (start_pos, end_pos) = self.get_start_and_end_pos(player);
let checker = async |pos: &BlockPos, world_inner: &Arc<World>| {
let state_id = world_inner.get_block_state_id(pos).await;
state_id != Block::AIR.default_state.id
};
let Some((block_pos, _direction)) = world.raycast(start_pos, end_pos, checker).await
else {
let (start_pos, end_pos) = self.get_start_and_end_pos(player);
let checker = async |pos: &BlockPos, w: &Arc<World>| {
w.get_block_state_id(pos).await != Block::AIR.default_state.id
};
if let Some((hit_pos, _)) = world.raycast(start_pos, end_pos, checker).await
&& world.get_block(&hit_pos).await == &Block::END_PORTAL_FRAME
{
return;
}
let origin = player.living_entity.entity.block_pos.load();
let target_block_pos = find_stronghold(&world, origin);
let Some(target) = target_block_pos else {
return;
};
let block = world.get_block(&block_pos).await;
let spawn_pos = Vector3::new(
player.living_entity.entity.pos.load().x,
player.living_entity.entity.pos.load().y
+ f64::from(EntityType::EYE_OF_ENDER.dimension[1]) * 0.5,
player.living_entity.entity.pos.load().z,
);
if block == &Block::END_PORTAL_FRAME {}
let entity = Entity::new(world.clone(), spawn_pos, &EntityType::EYE_OF_ENDER);
let eye = Arc::new(EyeOfEnder::new(entity));
let target_vec = Vector3::new(
f64::from(target.0.x),
f64::from(target.0.y),
f64::from(target.0.z),
);
eye.signal_to(target_vec).await;
world.spawn_entity(eye).await;
let pitch = 0.33f32 + rand::random::<f32>() * (0.5 - 0.33);
world
.play_sound_fine(
Sound::EntityEnderEyeLaunch,
SoundCategory::Neutral,
&spawn_pos,
1.0,
pitch,
)
.await;
player.inventory.held_item().lock().await.decrement(1);
})
//TODO Throw the Ender Eye in the direction of the stronghold.
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
fn find_stronghold(world: &Arc<World>, origin: BlockPos) -> Option<BlockPos> {
let level = &world.level;
let generator = &level.world_gen;
let seed = level.seed.0;
find_nearest_structure(
origin,
&[&StructureSet::get("strongholds").unwrap().placement],
100, // max search radius in chunks, matches vanilla default
seed as i64,
&generator.global_structure_cache,
)
}