mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
* feat(world): support generated loot minecarts * Minecart Vanila Parity * Rebased And Added Furnace, Hopper, TNT Minecarts * Refactor minecart variants into dedicated modules --------- Co-authored-by: Zoltán Virágh <zoltan.viragh@cloudtalk.io>
3883 lines
135 KiB
Rust
3883 lines
135 KiB
Rust
use crate::{
|
|
entity::item::ItemEntity,
|
|
net::{ClientPlatform, bedrock::BedrockClient, java::JavaClient},
|
|
server::Server,
|
|
world::{
|
|
World,
|
|
chunker::is_within_view_distance,
|
|
portal::{NetherPortal, PortalProcessor, PortalType, SourcePortalInfo},
|
|
},
|
|
};
|
|
use arc_swap::ArcSwap;
|
|
use bytes::BufMut;
|
|
use crossbeam::atomic::AtomicCell;
|
|
use living::LivingEntity;
|
|
use player::Player;
|
|
use pumpkin_data::BlockState;
|
|
use pumpkin_data::biome::Biome;
|
|
use pumpkin_data::block_properties::blocks_movement;
|
|
use pumpkin_data::data_component_impl::EquipmentSlot;
|
|
use pumpkin_data::dimension::Dimension;
|
|
use pumpkin_data::entity::EntityStatus;
|
|
use pumpkin_data::fluid::Fluid;
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_data::meta_data_type::MetaDataType;
|
|
use pumpkin_data::tag::{self, Taggable};
|
|
use pumpkin_data::tracked_data::TrackedData;
|
|
use pumpkin_data::{Block, BlockDirection};
|
|
use pumpkin_data::{
|
|
block_properties::{Facing, HorizontalFacing},
|
|
damage::DamageType,
|
|
entity::{EntityPose, EntityType},
|
|
sound::{Sound, SoundCategory},
|
|
};
|
|
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
|
|
use pumpkin_protocol::bedrock::client::{CAddActor, CSetActorMotion};
|
|
use pumpkin_protocol::codec::var_long::VarLong;
|
|
use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot};
|
|
use pumpkin_protocol::{
|
|
PositionFlag,
|
|
bedrock::client::{
|
|
move_actor_delta::{
|
|
CMoveActorDelta, MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW, MOVE_ACTOR_DELTA_FLAG_HAS_PITCH,
|
|
MOVE_ACTOR_DELTA_FLAG_HAS_X, MOVE_ACTOR_DELTA_FLAG_HAS_Y,
|
|
MOVE_ACTOR_DELTA_FLAG_HAS_YAW, MOVE_ACTOR_DELTA_FLAG_HAS_Z,
|
|
MOVE_ACTOR_DELTA_FLAG_ON_GROUND,
|
|
},
|
|
move_player::CMovePlayer,
|
|
set_actor_data::{
|
|
CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag,
|
|
entity_data_key,
|
|
},
|
|
},
|
|
codec::var_int::VarInt,
|
|
codec::var_ulong::VarULong,
|
|
java::client::play::{
|
|
CEntityPositionSync, CEntityVelocity, CHeadRot, CPlayerPosition, CSetEntityMetadata,
|
|
CSetPassengers, CSpawnEntity, CUpdateEntityRot, Metadata, MetadataSerializer,
|
|
},
|
|
};
|
|
use pumpkin_util::math::vector3::Axis;
|
|
use pumpkin_util::math::{
|
|
boundingbox::{BoundingBox, EntityDimensions},
|
|
get_section_cord,
|
|
position::BlockPos,
|
|
vector2::Vector2,
|
|
vector3::Vector3,
|
|
wrap_degrees,
|
|
};
|
|
use pumpkin_util::text::TextComponent;
|
|
use pumpkin_util::text::hover::HoverEvent;
|
|
use std::collections::{BTreeMap, HashSet};
|
|
use std::pin::Pin;
|
|
use std::sync::{
|
|
Arc,
|
|
atomic::{
|
|
AtomicBool, AtomicI32, AtomicU8, AtomicU32,
|
|
Ordering::{self, Relaxed},
|
|
},
|
|
};
|
|
use tokio::sync::Mutex;
|
|
use uuid::Uuid;
|
|
|
|
pub mod ageable;
|
|
pub mod ai;
|
|
pub mod area_effect_cloud;
|
|
pub mod attributes;
|
|
pub mod boss;
|
|
pub mod breath;
|
|
pub mod decoration;
|
|
pub mod effect;
|
|
pub mod experience_orb;
|
|
pub mod falling;
|
|
pub mod hunger;
|
|
pub mod item;
|
|
pub mod item_steerable;
|
|
pub mod living;
|
|
pub mod mob;
|
|
pub mod passive;
|
|
pub mod player;
|
|
pub mod projectile;
|
|
pub mod projectile_deflection;
|
|
pub mod tnt;
|
|
pub mod r#type;
|
|
pub mod vehicle;
|
|
|
|
mod combat;
|
|
pub mod predicate;
|
|
|
|
/// The maximum number of scoreboard tags an entity can carry, matching Vanilla.
|
|
pub const MAX_SCOREBOARD_TAGS: usize = 1024;
|
|
|
|
/// Returns the [`EntityStatus`] that should be broadcast when the given
|
|
/// equipment slot breaks.
|
|
#[must_use]
|
|
pub const fn equipment_break_status(slot: &EquipmentSlot) -> EntityStatus {
|
|
match slot {
|
|
EquipmentSlot::MainHand(_) => EntityStatus::MainhandBreak,
|
|
EquipmentSlot::OffHand(_) => EntityStatus::OffhandBreak,
|
|
EquipmentSlot::Head(_) => EntityStatus::HeadBreak,
|
|
EquipmentSlot::Chest(_) => EntityStatus::ChestBreak,
|
|
EquipmentSlot::Legs(_) => EntityStatus::LegsBreak,
|
|
EquipmentSlot::Feet(_) => EntityStatus::FeetBreak,
|
|
EquipmentSlot::Body(_) => EntityStatus::BodyBreak,
|
|
EquipmentSlot::Saddle(_) => EntityStatus::SaddleBreak,
|
|
}
|
|
}
|
|
|
|
pub type EntityBaseFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
|
|
|
pub type TeleportFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
|
|
|
|
pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
|
|
/// Called every tick for this entity.
|
|
///
|
|
/// The `caller` parameter is a reference to the entity that initiated the tick.
|
|
/// This can be the same entity the method is being called on (`self`),
|
|
/// but in some scenarios (e.g., interactions or events), it might be a different entity.
|
|
///
|
|
/// The `server` parameter provides access to the game server instance.
|
|
fn tick<'a>(
|
|
&'a self,
|
|
caller: &'a Arc<dyn EntityBase>,
|
|
server: &'a Server,
|
|
) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
if let Some(living) = self.get_living_entity() {
|
|
living.tick(caller, server).await;
|
|
} else {
|
|
self.get_entity().tick(caller, server).await;
|
|
}
|
|
})
|
|
}
|
|
|
|
fn get_job_site_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
|
None
|
|
}
|
|
|
|
fn get_home_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
|
None
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self
|
|
}
|
|
|
|
fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> {
|
|
None
|
|
}
|
|
|
|
fn get_eye_pos(&self) -> Vector3<f64> {
|
|
self.get_entity().get_eye_pos()
|
|
}
|
|
|
|
fn get_looking_vector(&self) -> Vector3<f64> {
|
|
let entity = self.get_entity();
|
|
Vector3::from_yaw_pitch(entity.yaw.load(), entity.pitch.load())
|
|
}
|
|
|
|
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
let entity = self.get_entity();
|
|
|
|
// If the internal age is negative, it's a baby
|
|
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
|
|
|
|
if is_baby {
|
|
let mut bedrock_meta = EntityMetadata::new();
|
|
bedrock_meta.set_flag(entity_data_key::FLAGS, entity_data_flag::BABY as u8, true);
|
|
entity.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::BABY_ID,
|
|
MetaDataType::BOOLEAN,
|
|
true,
|
|
)],
|
|
Some(&bedrock_meta),
|
|
);
|
|
}
|
|
})
|
|
}
|
|
fn set_variant_name(&self, _name: &str) {}
|
|
|
|
// This method takes ownership of Arc<Self>, so the lifetime bounds are different.
|
|
fn teleport(
|
|
self: Arc<Self>,
|
|
position: Vector3<f64>,
|
|
yaw: Option<f32>,
|
|
pitch: Option<f32>,
|
|
world: Arc<World>,
|
|
) -> TeleportFuture
|
|
where
|
|
Self: 'static,
|
|
{
|
|
Box::pin(async move {
|
|
self.get_entity().teleport(position, yaw, pitch, world);
|
|
})
|
|
}
|
|
|
|
fn is_pushed_by_fluids(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
/// Whether the entity is immune from explosion knockback and damage
|
|
fn is_immune_to_explosion(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn get_gravity(&self) -> f64 {
|
|
0.0
|
|
}
|
|
|
|
fn get_mob(&self) -> Option<&dyn mob::Mob> {
|
|
None
|
|
}
|
|
|
|
fn tick_in_void<'a>(&'a self, _dyn_self: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move { self.get_entity().remove().await })
|
|
}
|
|
|
|
/// Returns if damage was successful or not
|
|
fn damage<'a>(
|
|
&'a self,
|
|
caller: &'a dyn EntityBase,
|
|
amount: f32,
|
|
damage_type: DamageType,
|
|
) -> EntityBaseFuture<'a, bool> {
|
|
Box::pin(async move {
|
|
caller
|
|
.damage_with_context(caller, amount, damage_type, None, None, None)
|
|
.await
|
|
})
|
|
}
|
|
|
|
fn is_spectator(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn is_collidable(&self, _entity: Option<Box<dyn EntityBase>>) -> bool {
|
|
false
|
|
}
|
|
|
|
fn can_hit(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn is_flutterer(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Custom Y-axis velocity drag multiplier applied during `travel_in_air`.
|
|
/// Bats return `Some(0.6)` to match vanilla's `travel()` override.
|
|
fn get_y_velocity_drag(&self) -> Option<f64> {
|
|
None
|
|
}
|
|
|
|
fn send_bedrock_spawn_packet<'a>(
|
|
&'a self,
|
|
client: &'a BedrockClient,
|
|
) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
let entity = self.get_entity();
|
|
let runtime_id = entity.entity_id as u64;
|
|
let packet = CAddActor::new(
|
|
VarLong(runtime_id as i64),
|
|
VarULong(runtime_id),
|
|
self.get_entity().entity_type.resource_name.to_string(),
|
|
entity.pos.load().to_f32_lossy(),
|
|
entity.velocity.load().to_f32_lossy(),
|
|
entity.pitch.load(),
|
|
entity.yaw.load(),
|
|
entity.head_yaw.load(),
|
|
entity.body_yaw.load(),
|
|
Vec::new(),
|
|
entity.bedrock_metadata(),
|
|
PropertySyncData {
|
|
int_properties: std::collections::HashMap::new(),
|
|
float_properties: std::collections::HashMap::new(),
|
|
},
|
|
Vec::new(),
|
|
);
|
|
client.send_game_packet(&packet).await;
|
|
})
|
|
}
|
|
|
|
fn send_java_spawn_packet<'a>(&'a self, client: &'a JavaClient) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
client
|
|
.enqueue_packet(&self.get_entity().create_spawn_packet())
|
|
.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> {
|
|
Box::pin(async move {
|
|
if caller.get_living_entity().is_some() {
|
|
return caller
|
|
.damage_with_context(caller, amount, damage_type, position, source, cause)
|
|
.await;
|
|
}
|
|
false
|
|
})
|
|
}
|
|
|
|
/// Called when a player right-clicks this entity with an item.
|
|
/// Returns true if the interaction was handled.
|
|
fn interact<'a>(
|
|
&'a self,
|
|
_player: &'a Arc<Player>,
|
|
_item_stack: &'a mut ItemStack,
|
|
) -> EntityBaseFuture<'a, bool> {
|
|
Box::pin(async { false })
|
|
}
|
|
|
|
fn set_on_fire_for(&self, seconds: f32) {
|
|
let entity = self.get_entity();
|
|
// Exclude fire-immune entities (ex. certain items) from burn damage
|
|
if !entity.fire_immune.load(Ordering::Relaxed) {
|
|
self.set_on_fire_for_ticks((seconds * 20.0).floor() as u32);
|
|
}
|
|
}
|
|
|
|
fn set_on_fire_for_ticks(&self, ticks: u32) {
|
|
let entity = self.get_entity();
|
|
if entity.fire_ticks.load(Ordering::Relaxed) < ticks as i32 {
|
|
entity.fire_ticks.store(ticks as i32, Ordering::Relaxed);
|
|
}
|
|
// TODO: defrost
|
|
}
|
|
|
|
/// Called when a player collides with a entity
|
|
fn on_player_collision<'a>(&'a self, _player: &'a Arc<Player>) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async {})
|
|
}
|
|
|
|
fn is_passenger(&self) -> EntityBaseFuture<'_, bool> {
|
|
Box::pin(async move { self.get_entity().has_vehicle().await })
|
|
}
|
|
|
|
fn is_vehicle(&self) -> EntityBaseFuture<'_, bool> {
|
|
Box::pin(async move { self.get_entity().has_passengers().await })
|
|
}
|
|
|
|
fn has_passenger<'a>(&'a self, other: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, bool> {
|
|
Box::pin(async move {
|
|
self.get_entity()
|
|
.passengers
|
|
.lock()
|
|
.await
|
|
.iter()
|
|
.any(|p| p.get_entity().entity_id == other.get_entity().entity_id)
|
|
})
|
|
}
|
|
|
|
fn move_entity<'a>(
|
|
&'a self,
|
|
caller: &'a Arc<dyn EntityBase>,
|
|
motion: Vector3<f64>,
|
|
) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.get_entity().move_entity(caller, motion).await;
|
|
})
|
|
}
|
|
|
|
fn is_pushable(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn push<'a>(&'a self, entity: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
let self_entity = self.get_entity();
|
|
let other_entity = entity.get_entity();
|
|
|
|
if self_entity.no_clip.load(Ordering::Relaxed)
|
|
|| other_entity.no_clip.load(Ordering::Relaxed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
{
|
|
let passengers = self_entity.passengers.lock().await;
|
|
if passengers
|
|
.iter()
|
|
.any(|p| p.get_entity().entity_id == other_entity.entity_id)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
{
|
|
let passengers = other_entity.passengers.lock().await;
|
|
if passengers
|
|
.iter()
|
|
.any(|p| p.get_entity().entity_id == self_entity.entity_id)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
let mut dx = other_entity.pos.load().x - self_entity.pos.load().x;
|
|
let mut dz = other_entity.pos.load().z - self_entity.pos.load().z;
|
|
let mut d = dx.abs().max(dz.abs());
|
|
if d >= 0.01 {
|
|
d = d.sqrt();
|
|
dx /= d;
|
|
dz /= d;
|
|
let mut d2 = 1.0 / d;
|
|
if d2 > 1.0 {
|
|
d2 = 1.0;
|
|
}
|
|
dx *= d2;
|
|
dz *= d2;
|
|
dx *= 0.05;
|
|
dz *= 0.05;
|
|
|
|
if !self_entity.has_passengers().await && self.is_pushable() {
|
|
let mut vel = self_entity.velocity.load();
|
|
vel.x -= dx;
|
|
vel.z -= dz;
|
|
self_entity.velocity.store(vel);
|
|
self_entity.send_velocity();
|
|
}
|
|
|
|
if !other_entity.has_passengers().await && entity.is_pushable() {
|
|
let mut vel = other_entity.velocity.load();
|
|
vel.x += dx;
|
|
vel.z += dz;
|
|
other_entity.velocity.store(vel);
|
|
other_entity.send_velocity();
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
fn push_entities<'a>(
|
|
&'a self,
|
|
dyn_self: &'a Arc<dyn EntityBase>,
|
|
) -> EntityBaseFuture<'a, bool> {
|
|
Box::pin(async move {
|
|
let mut picked_up = false;
|
|
let mut pushed = false;
|
|
let self_entity = self.get_entity();
|
|
let entity_bb = self_entity.bounding_box.load();
|
|
|
|
if !self.is_pushable() {
|
|
return false;
|
|
}
|
|
|
|
let world = self_entity.world.load();
|
|
|
|
let is_rideable_minecart = self_entity.entity_type.id == EntityType::MINECART.id;
|
|
let is_abstract_minecart = is_rideable_minecart
|
|
|| self_entity.entity_type.id == EntityType::CHEST_MINECART.id
|
|
|| self_entity.entity_type.id == EntityType::COMMAND_BLOCK_MINECART.id
|
|
|| self_entity.entity_type.id == EntityType::FURNACE_MINECART.id
|
|
|| self_entity.entity_type.id == EntityType::HOPPER_MINECART.id
|
|
|| self_entity.entity_type.id == EntityType::SPAWNER_MINECART.id
|
|
|| self_entity.entity_type.id == EntityType::TNT_MINECART.id;
|
|
|
|
let is_minecart_fn = |id| -> bool {
|
|
id == EntityType::MINECART.id
|
|
|| id == EntityType::CHEST_MINECART.id
|
|
|| id == EntityType::COMMAND_BLOCK_MINECART.id
|
|
|| id == EntityType::FURNACE_MINECART.id
|
|
|| id == EntityType::HOPPER_MINECART.id
|
|
|| id == EntityType::SPAWNER_MINECART.id
|
|
|| id == EntityType::TNT_MINECART.id
|
|
};
|
|
|
|
if is_abstract_minecart {
|
|
let is_vehicle = self.is_vehicle().await;
|
|
|
|
if is_rideable_minecart && !is_vehicle {
|
|
let pickup_bb = entity_bb.expand(0.2, 0.0, 0.2);
|
|
let other_entities = world.get_entities_at_box(&pickup_bb);
|
|
|
|
for other in other_entities {
|
|
if other.get_entity().entity_id != self_entity.entity_id {
|
|
let other_type = other.get_entity().entity_type.id;
|
|
let is_iron_golem = other_type == EntityType::IRON_GOLEM.id;
|
|
let is_other_minecart = is_minecart_fn(other_type);
|
|
|
|
if !is_iron_golem
|
|
&& !is_other_minecart
|
|
&& !other.is_passenger().await
|
|
&& other.is_pushable()
|
|
&& other.get_entity().riding_cooldown.load(Relaxed) == 0
|
|
{
|
|
dyn_self
|
|
.get_entity()
|
|
.add_passenger(dyn_self.clone(), other.clone())
|
|
.await;
|
|
picked_up = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let push_bb = entity_bb.expand(1.0e-7, 1.0e-7, 1.0e-7);
|
|
|
|
let other_entities = world.get_entities_at_box(&push_bb);
|
|
for other in other_entities {
|
|
if other.get_entity().entity_id != self_entity.entity_id {
|
|
let other_type = other.get_entity().entity_type.id;
|
|
let is_other_minecart = is_minecart_fn(other_type);
|
|
let is_iron_golem = other_type == EntityType::IRON_GOLEM.id;
|
|
|
|
if is_rideable_minecart {
|
|
if (is_iron_golem
|
|
|| is_other_minecart
|
|
|| is_vehicle
|
|
|| !other.get_entity().has_vehicle().await)
|
|
&& other.is_pushable()
|
|
{
|
|
dyn_self.push(&other).await;
|
|
pushed = true;
|
|
}
|
|
} else if !self.has_passenger(&other).await
|
|
&& other.is_pushable()
|
|
&& is_other_minecart
|
|
{
|
|
dyn_self.push(&other).await;
|
|
pushed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let players = world.get_players_at_box(&push_bb);
|
|
for player in players {
|
|
if player.get_entity().entity_id != self_entity.entity_id
|
|
&& is_rideable_minecart
|
|
{
|
|
let player_base: Arc<dyn EntityBase> = player.clone();
|
|
dyn_self.push(&player_base).await;
|
|
pushed = true;
|
|
// Non-rideable minecarts (hoppers, chests) do not push players in vanilla.
|
|
}
|
|
}
|
|
} else {
|
|
let other_entities = world.get_entities_at_box(&entity_bb);
|
|
for other in other_entities {
|
|
if other.get_entity().entity_id != self_entity.entity_id {
|
|
dyn_self.push(&other).await;
|
|
pushed = true;
|
|
}
|
|
}
|
|
|
|
let players = world.get_players_at_box(&entity_bb);
|
|
for player in players {
|
|
if player.get_entity().entity_id != self_entity.entity_id {
|
|
let player_base: Arc<dyn EntityBase> = player.clone();
|
|
dyn_self.push(&player_base).await;
|
|
pushed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
picked_up && !pushed
|
|
})
|
|
}
|
|
|
|
fn on_hit(&self, _hit: crate::entity::projectile::ProjectileHit) -> EntityBaseFuture<'_, ()> {
|
|
Box::pin(async {})
|
|
}
|
|
|
|
fn set_paddle_state(&self, _left: bool, _right: bool) -> EntityBaseFuture<'_, ()> {
|
|
Box::pin(async {})
|
|
}
|
|
|
|
fn is_in_love(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn is_breeding_ready(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn reset_love(&self) {}
|
|
|
|
fn set_breeding_cooldown(&self, _ticks: i32) {}
|
|
|
|
fn is_panicking(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn get_entity(&self) -> &Entity;
|
|
|
|
fn get_living_entity(&self) -> Option<&LivingEntity>;
|
|
|
|
fn cast_any(&self) -> &dyn std::any::Any;
|
|
|
|
fn get_item_entity(self: Arc<Self>) -> Option<Arc<ItemEntity>> {
|
|
None
|
|
}
|
|
|
|
fn get_player(&self) -> Option<&Player> {
|
|
None
|
|
}
|
|
|
|
/// Should return the name of the entity without click or hover events.
|
|
fn get_name(&self) -> TextComponent {
|
|
let entity = self.get_entity();
|
|
entity
|
|
.custom_name
|
|
.load()
|
|
.as_ref()
|
|
.clone()
|
|
.unwrap_or(TextComponent::translate_cross(
|
|
format!("entity.minecraft.{}", entity.entity_type.resource_name),
|
|
format!("entity.minecraft.{}", entity.entity_type.resource_name),
|
|
[],
|
|
))
|
|
}
|
|
|
|
fn get_display_name(&self) -> EntityBaseFuture<'_, TextComponent> {
|
|
Box::pin(async move {
|
|
// TODO: team color
|
|
let entity = self.get_entity();
|
|
let mut name = entity.custom_name.load().as_ref().clone().unwrap_or(
|
|
TextComponent::translate_cross(
|
|
format!("entity.minecraft.{}", entity.entity_type.resource_name),
|
|
format!("entity.minecraft.{}", entity.entity_type.resource_name),
|
|
[],
|
|
),
|
|
);
|
|
let name_clone = name.clone();
|
|
name = name.hover_event(HoverEvent::show_entity(
|
|
entity.entity_uuid.to_string(),
|
|
entity.entity_type.resource_name.into(),
|
|
Some(name_clone),
|
|
));
|
|
name = name.insertion(entity.entity_uuid.to_string());
|
|
name
|
|
})
|
|
}
|
|
|
|
/// Kills the Entity.
|
|
fn kill<'a>(&'a self, caller: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
if self.get_living_entity().is_some() {
|
|
caller
|
|
.damage(caller, f32::MAX, DamageType::GENERIC_KILL)
|
|
.await;
|
|
} else {
|
|
// TODO this should be removed once all entities are implemented
|
|
self.get_entity().remove().await;
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Returns itself as the nbt storage for saving and loading data.
|
|
fn as_nbt_storage(&self) -> &dyn NBTStorage;
|
|
|
|
fn get_experience_reward(&self, _killer: Option<&dyn EntityBase>) -> u32 {
|
|
0
|
|
}
|
|
|
|
fn get_base_experience_reward(&self) -> u32 {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
|
pub enum RemovalReason {
|
|
Killed,
|
|
Discarded,
|
|
UnloadedToChunk,
|
|
UnloadedWithPlayer,
|
|
ChangedDimension,
|
|
}
|
|
|
|
impl RemovalReason {
|
|
#[must_use]
|
|
pub const fn should_destroy(&self) -> bool {
|
|
match self {
|
|
Self::Killed | Self::Discarded => true,
|
|
Self::UnloadedToChunk | Self::UnloadedWithPlayer | Self::ChangedDimension => false,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn should_save(&self) -> bool {
|
|
match self {
|
|
Self::Killed | Self::Discarded | Self::UnloadedWithPlayer | Self::ChangedDimension => {
|
|
false
|
|
}
|
|
Self::UnloadedToChunk => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
// IMPORTANT: have that 1 and not 0 because fetch_add returns previous value and 0 would be invalid
|
|
static CURRENT_ID: AtomicI32 = AtomicI32::new(1);
|
|
|
|
/// Represents a non-living Entity (e.g. Item, Egg, Snowball...)
|
|
pub struct Entity {
|
|
/// A unique identifier for the entity
|
|
pub entity_id: i32,
|
|
/// A persistent, unique identifier for the entity
|
|
pub entity_uuid: uuid::Uuid,
|
|
/// The type of entity (e.g., player, zombie, item)
|
|
pub entity_type: &'static EntityType,
|
|
/// The world in which the entity exists.
|
|
/// Uses `ArcSwap` to allow atomic updates when changing dimensions.
|
|
pub world: ArcSwap<World>,
|
|
/// The entity's current position in the world
|
|
pub pos: AtomicCell<Vector3<f64>>,
|
|
/// The last known position of the entity.
|
|
pub last_pos: AtomicCell<Vector3<f64>>,
|
|
/// The last movement vector
|
|
pub movement: AtomicCell<Vector3<f64>>,
|
|
/// The entity's position rounded to the nearest block coordinates
|
|
pub block_pos: AtomicCell<BlockPos>,
|
|
/// The block supporting the entity
|
|
pub supporting_block_pos: AtomicCell<Option<BlockPos>>,
|
|
/// The chunk coordinates of the entity's current position
|
|
pub chunk_pos: AtomicCell<Vector2<i32>>,
|
|
/// Indicates whether the entity is sneaking
|
|
pub sneaking: AtomicBool,
|
|
/// Indicates whether the entity is sprinting
|
|
pub sprinting: AtomicBool,
|
|
/// Indicates whether the entity is swimming
|
|
pub swimming: AtomicBool,
|
|
/// Indicates whether the entity is invisible
|
|
pub invisible: AtomicBool,
|
|
/// Indicates whether the entity is glowing
|
|
pub glowing: AtomicBool,
|
|
/// Indicates whether the entity is flying due to a fall
|
|
pub fall_flying: AtomicBool,
|
|
/// The entity's current velocity vector, aka knockback
|
|
pub velocity: AtomicCell<Vector3<f64>>,
|
|
/// Tracks a horizontal collision
|
|
pub horizontal_collision: AtomicBool,
|
|
/// Indicates whether the entity is on the ground (may not always be accurate).
|
|
pub on_ground: AtomicBool,
|
|
/// Indicates whether the entity is touching water
|
|
pub touching_water: AtomicBool,
|
|
/// Indicates the fluid height
|
|
pub water_height: AtomicCell<f64>,
|
|
/// Indicates whether the entity is touching lava
|
|
pub touching_lava: AtomicBool,
|
|
/// Indicates the fluid height
|
|
pub lava_height: AtomicCell<f64>,
|
|
/// The entity's yaw rotation (horizontal rotation) ← →
|
|
pub yaw: AtomicCell<f32>,
|
|
/// The entity's head yaw rotation (horizontal rotation of the head)
|
|
pub head_yaw: AtomicCell<f32>,
|
|
/// The entity's body yaw rotation (horizontal rotation of the body)
|
|
pub body_yaw: AtomicCell<f32>,
|
|
/// The entity's pitch rotation (vertical rotation) ↑ ↓
|
|
pub pitch: AtomicCell<f32>,
|
|
/// The entity's current pose (e.g., standing, sitting, swimming).
|
|
pub pose: AtomicCell<EntityPose>,
|
|
/// The bounding box of an entity (hitbox)
|
|
pub bounding_box: AtomicCell<BoundingBox>,
|
|
///The size (width and height) of the bounding box
|
|
pub entity_dimension: AtomicCell<EntityDimensions>,
|
|
/// Whether this entity is invulnerable to all damage
|
|
pub invulnerable: AtomicBool,
|
|
/// List of damage types this entity is immune to
|
|
pub damage_immunities: Mutex<Vec<DamageType>>,
|
|
// Whether the entity is immune to fire (to disable visual fire and fire damage)
|
|
pub fire_immune: AtomicBool,
|
|
pub fire_ticks: AtomicI32,
|
|
pub has_visual_fire: AtomicBool,
|
|
/// The number of ticks the entity has been frozen (in powder snow)
|
|
/// Max is 140 ticks (7 seconds). Increases by 1/tick in powder snow, decreases by 2/tick outside.
|
|
pub frozen_ticks: AtomicI32,
|
|
/// Set during block-collision processing when the entity is touching powder snow.
|
|
pub is_in_powder_snow: AtomicBool,
|
|
/// True if the entity was in powder snow during the previous tick.
|
|
pub was_in_powder_snow: AtomicBool,
|
|
pub removal_reason: AtomicCell<Option<RemovalReason>>,
|
|
// The passengers that entity has
|
|
pub passengers: Mutex<Vec<Arc<dyn EntityBase>>>,
|
|
/// The vehicle that entity is in
|
|
pub vehicle: Mutex<Option<Arc<dyn EntityBase>>>,
|
|
/// The entity this entity is attached/leashed to (if any)
|
|
pub leashed_to: Mutex<Option<Arc<dyn EntityBase>>>,
|
|
/// Cooldown before entity can mount again after dismounting
|
|
pub riding_cooldown: AtomicI32,
|
|
/// The age of the entity in ticks. Negative values indicate a baby.
|
|
pub age: AtomicI32,
|
|
|
|
pub current_biome: ArcSwap<&'static Biome>,
|
|
pub last_biome_update_pos: AtomicCell<BlockPos>,
|
|
|
|
pub portal_cooldown: AtomicU32,
|
|
|
|
pub portal_manager: Mutex<Option<Mutex<PortalProcessor>>>,
|
|
/// Custom name for the entity
|
|
pub custom_name: ArcSwap<Option<TextComponent>>,
|
|
/// Indicates whether the entity's custom name is visible
|
|
pub custom_name_visible: AtomicBool,
|
|
pub silent: AtomicBool,
|
|
pub has_no_gravity: AtomicBool,
|
|
/// Scoreboard tags attached to this entity, managed with `/tag`.
|
|
/// Vanilla allows at most [`MAX_SCOREBOARD_TAGS`] tags per entity.
|
|
pub scoreboard_tags: Mutex<HashSet<String>>,
|
|
/// The data send in the Entity Spawn packet
|
|
pub data: AtomicI32,
|
|
/// Stores entity boolean flags (on fire, sneaking, invisible, glowing, etc.)
|
|
pub flags: std::sync::atomic::AtomicI8,
|
|
/// Stores Bedrock-specific entity boolean flags (bit 0-63)
|
|
pub bedrock_flags: std::sync::atomic::AtomicI64,
|
|
/// Stores more Bedrock-specific entity boolean flags (bit 0-63)
|
|
pub bedrock_flags_two: std::sync::atomic::AtomicI64,
|
|
/// If true, the entity cannot collide with anything (e.g. spectator)
|
|
pub no_clip: AtomicBool,
|
|
/// Multiplies movement for one tick before being reset
|
|
pub movement_multiplier: AtomicCell<Vector3<f64>>,
|
|
/// Determines whether the entity's velocity needs to be sent
|
|
pub velocity_dirty: AtomicBool,
|
|
/// Set when an Entity is to be removed but could still be referenced
|
|
pub removed: AtomicBool,
|
|
/// The last sent yaw value (encoded as u8) for change detection
|
|
pub last_sent_yaw: AtomicU8,
|
|
/// The last sent pitch value (encoded as u8) for change detection
|
|
pub last_sent_pitch: AtomicU8,
|
|
/// Cache for the last sent position to optimize Entity Pos update packets
|
|
pub last_sent_pos: AtomicCell<Vector3<f64>>,
|
|
/// Cache for the last sent head yaw byte
|
|
pub last_sent_head_yaw: AtomicU8,
|
|
}
|
|
|
|
impl Entity {
|
|
pub fn new(
|
|
world: Arc<World>,
|
|
position: Vector3<f64>,
|
|
entity_type: &'static EntityType,
|
|
) -> Self {
|
|
Self::from_uuid(Uuid::new_v4(), world, position, entity_type)
|
|
}
|
|
|
|
pub fn reserve_ids(count: i32) -> i32 {
|
|
CURRENT_ID.fetch_add(count, Relaxed)
|
|
}
|
|
|
|
pub fn from_uuid(
|
|
entity_uuid: uuid::Uuid,
|
|
world: Arc<World>,
|
|
position: Vector3<f64>,
|
|
entity_type: &'static EntityType,
|
|
) -> Self {
|
|
Self::from_uuid_with_id(
|
|
CURRENT_ID.fetch_add(1, Relaxed),
|
|
entity_uuid,
|
|
world,
|
|
position,
|
|
entity_type,
|
|
)
|
|
}
|
|
|
|
pub fn from_uuid_with_id(
|
|
entity_id: i32,
|
|
entity_uuid: uuid::Uuid,
|
|
world: Arc<World>,
|
|
position: Vector3<f64>,
|
|
entity_type: &'static EntityType,
|
|
) -> Self {
|
|
let floor_x = position.x.floor() as i32;
|
|
let floor_y = position.y.floor() as i32;
|
|
let floor_z = position.z.floor() as i32;
|
|
|
|
let bounding_box_size = EntityDimensions {
|
|
width: entity_type.dimension[0],
|
|
height: entity_type.dimension[1],
|
|
eye_height: entity_type.eye_height,
|
|
};
|
|
|
|
Self {
|
|
entity_id,
|
|
entity_uuid,
|
|
entity_type,
|
|
on_ground: AtomicBool::new(false),
|
|
touching_water: AtomicBool::new(false),
|
|
water_height: AtomicCell::new(0.0),
|
|
touching_lava: AtomicBool::new(false),
|
|
lava_height: AtomicCell::new(0.0),
|
|
horizontal_collision: AtomicBool::new(false),
|
|
pos: AtomicCell::new(position),
|
|
last_pos: AtomicCell::new(position),
|
|
movement: AtomicCell::new(Vector3::default()),
|
|
block_pos: AtomicCell::new(BlockPos(Vector3::new(floor_x, floor_y, floor_z))),
|
|
supporting_block_pos: AtomicCell::new(None),
|
|
chunk_pos: AtomicCell::new(Vector2::new(
|
|
get_section_cord(floor_x),
|
|
get_section_cord(floor_z),
|
|
)),
|
|
sneaking: AtomicBool::new(false),
|
|
swimming: AtomicBool::new(false),
|
|
invisible: AtomicBool::new(false),
|
|
glowing: AtomicBool::new(false),
|
|
world: ArcSwap::new(world),
|
|
sprinting: AtomicBool::new(false),
|
|
fall_flying: AtomicBool::new(false),
|
|
yaw: AtomicCell::new(0.0),
|
|
head_yaw: AtomicCell::new(0.0),
|
|
body_yaw: AtomicCell::new(0.0),
|
|
pitch: AtomicCell::new(0.0),
|
|
velocity: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)),
|
|
pose: AtomicCell::new(EntityPose::Standing),
|
|
bounding_box: AtomicCell::new(BoundingBox::new_from_pos(
|
|
position.x,
|
|
position.y,
|
|
position.z,
|
|
&bounding_box_size,
|
|
)),
|
|
entity_dimension: AtomicCell::new(bounding_box_size),
|
|
invulnerable: AtomicBool::new(false),
|
|
damage_immunities: Mutex::new(Vec::new()),
|
|
data: AtomicI32::new(0),
|
|
flags: std::sync::atomic::AtomicI8::new(0),
|
|
bedrock_flags: std::sync::atomic::AtomicI64::new(0),
|
|
bedrock_flags_two: std::sync::atomic::AtomicI64::new(0),
|
|
fire_immune: AtomicBool::new(false),
|
|
fire_ticks: AtomicI32::new(-1),
|
|
has_visual_fire: AtomicBool::new(false),
|
|
frozen_ticks: AtomicI32::new(0),
|
|
is_in_powder_snow: AtomicBool::new(false),
|
|
was_in_powder_snow: AtomicBool::new(false),
|
|
removal_reason: AtomicCell::new(None),
|
|
passengers: Mutex::new(Vec::new()),
|
|
vehicle: Mutex::new(None),
|
|
leashed_to: Mutex::new(None),
|
|
|
|
riding_cooldown: AtomicI32::new(0),
|
|
age: AtomicI32::new(0),
|
|
current_biome: ArcSwap::new(Arc::new(&Biome::PLAINS)),
|
|
last_biome_update_pos: AtomicCell::new(BlockPos::new(floor_x, floor_y, floor_z)),
|
|
portal_cooldown: AtomicU32::new(0),
|
|
portal_manager: Mutex::new(None),
|
|
custom_name: ArcSwap::new(Arc::new(None)),
|
|
custom_name_visible: AtomicBool::new(false),
|
|
silent: AtomicBool::new(false),
|
|
has_no_gravity: AtomicBool::new(false),
|
|
scoreboard_tags: Mutex::new(HashSet::new()),
|
|
no_clip: AtomicBool::new(false),
|
|
movement_multiplier: AtomicCell::new(Vector3::default()),
|
|
velocity_dirty: AtomicBool::new(true),
|
|
removed: AtomicBool::new(false),
|
|
last_sent_yaw: AtomicU8::new(0),
|
|
last_sent_pitch: AtomicU8::new(0),
|
|
last_sent_head_yaw: AtomicU8::new(0),
|
|
last_sent_pos: AtomicCell::new(position),
|
|
}
|
|
}
|
|
|
|
pub fn add_velocity(&self, velocity: Vector3<f64>) {
|
|
self.set_velocity(self.velocity.load() + velocity);
|
|
}
|
|
|
|
pub fn set_velocity(&self, velocity: Vector3<f64>) {
|
|
self.velocity.store(velocity);
|
|
self.send_velocity();
|
|
}
|
|
|
|
/// Updates the world reference for this entity.
|
|
/// Called when the entity changes dimensions (e.g., through a nether portal).
|
|
pub fn set_world(&self, world: Arc<World>) {
|
|
self.world.store(world);
|
|
}
|
|
|
|
pub fn bedrock_metadata(&self) -> EntityMetadata {
|
|
if self.bedrock_flags.load(Ordering::Relaxed) == 0 {
|
|
self.bedrock_flags.fetch_or(
|
|
(1i64 << entity_data_flag::HAS_GRAVITY)
|
|
| (1i64 << entity_data_flag::CLIMB)
|
|
| (1i64 << entity_data_flag::HAS_COLLISION)
|
|
| (1i64 << entity_data_flag::BREATHING),
|
|
Ordering::Relaxed,
|
|
);
|
|
}
|
|
|
|
let mut metadata = EntityMetadata::new();
|
|
metadata.set(
|
|
entity_data_key::WIDTH,
|
|
MetadataValue::Float(self.entity_type.dimension[0]),
|
|
);
|
|
metadata.set(
|
|
entity_data_key::HEIGHT,
|
|
MetadataValue::Float(self.entity_type.dimension[1]),
|
|
);
|
|
metadata.set(entity_data_key::SCALE, MetadataValue::Float(1.0));
|
|
metadata.set(
|
|
entity_data_key::FLAGS,
|
|
MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)),
|
|
);
|
|
metadata.set(
|
|
entity_data_key::FLAGS_TWO,
|
|
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
|
|
);
|
|
|
|
if let Some(name) = &**self.custom_name.load() {
|
|
metadata.set(
|
|
entity_data_key::NAME,
|
|
MetadataValue::String(name.clone().get_text()),
|
|
);
|
|
if self.custom_name_visible.load(Ordering::Relaxed) {
|
|
metadata.set_flag(
|
|
entity_data_key::FLAGS,
|
|
entity_data_flag::SHOW_NAME as u8,
|
|
true,
|
|
);
|
|
metadata.set_flag(
|
|
entity_data_key::FLAGS,
|
|
entity_data_flag::ALWAYS_SHOW_NAME as u8,
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
|
|
metadata
|
|
}
|
|
|
|
/// Sets the entity's age in ticks.
|
|
/// Negative values indicate that the entity is a baby.
|
|
pub fn set_age(&self, age: i32) {
|
|
self.age.store(age, Relaxed);
|
|
}
|
|
|
|
/// Adds a scoreboard tag to this entity.
|
|
///
|
|
/// Returns `false` if the entity already has the tag or already carries
|
|
/// [`MAX_SCOREBOARD_TAGS`] tags.
|
|
pub async fn add_scoreboard_tag(&self, tag: &str) -> bool {
|
|
let mut tags = self.scoreboard_tags.lock().await;
|
|
tags.len() < MAX_SCOREBOARD_TAGS && tags.insert(tag.to_owned())
|
|
}
|
|
|
|
/// Removes a scoreboard tag from this entity.
|
|
///
|
|
/// Returns `false` if the entity did not have the tag.
|
|
pub async fn remove_scoreboard_tag(&self, tag: &str) -> bool {
|
|
self.scoreboard_tags.lock().await.remove(tag)
|
|
}
|
|
|
|
/// Sets a custom name for the entity, typically used with nametags
|
|
pub fn set_custom_name(&self, name: TextComponent) {
|
|
self.custom_name.store(Arc::new(Some(name.clone())));
|
|
let mut bedrock_meta = EntityMetadata::new();
|
|
bedrock_meta.set(
|
|
entity_data_key::NAME,
|
|
MetadataValue::String(name.clone().get_text()),
|
|
);
|
|
let visible = self.custom_name_visible.load(Ordering::Relaxed);
|
|
bedrock_meta.set_flag(
|
|
entity_data_key::FLAGS,
|
|
entity_data_flag::SHOW_NAME as u8,
|
|
visible,
|
|
);
|
|
bedrock_meta.set_flag(
|
|
entity_data_key::FLAGS,
|
|
entity_data_flag::ALWAYS_SHOW_NAME as u8,
|
|
visible,
|
|
);
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::CUSTOM_NAME,
|
|
MetaDataType::OPTIONAL_TEXT_COMPONENT,
|
|
Some(name),
|
|
)],
|
|
Some(&bedrock_meta),
|
|
);
|
|
}
|
|
|
|
pub fn set_custom_name_visible(&self, visible: bool) {
|
|
self.custom_name_visible.store(visible, Ordering::Relaxed);
|
|
let mut bedrock_meta = EntityMetadata::new();
|
|
if let Some(name) = &**self.custom_name.load() {
|
|
bedrock_meta.set(
|
|
entity_data_key::NAME,
|
|
MetadataValue::String(name.clone().get_text()),
|
|
);
|
|
}
|
|
bedrock_meta.set_flag(
|
|
entity_data_key::FLAGS,
|
|
entity_data_flag::SHOW_NAME as u8,
|
|
visible,
|
|
);
|
|
bedrock_meta.set_flag(
|
|
entity_data_key::FLAGS,
|
|
entity_data_flag::ALWAYS_SHOW_NAME as u8,
|
|
visible,
|
|
);
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::CUSTOM_NAME_VISIBLE,
|
|
MetaDataType::BOOLEAN,
|
|
visible,
|
|
)],
|
|
Some(&bedrock_meta),
|
|
);
|
|
}
|
|
|
|
pub fn is_silent(&self) -> bool {
|
|
self.silent.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn set_silent(&self, silent: bool) {
|
|
self.silent.store(silent, Ordering::Relaxed);
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::SILENT,
|
|
MetaDataType::BOOLEAN,
|
|
silent,
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn has_no_gravity(&self) -> bool {
|
|
self.has_no_gravity.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn set_has_no_gravity(&self, no_gravity: bool) {
|
|
self.has_no_gravity.store(no_gravity, Ordering::Relaxed);
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::NO_GRAVITY,
|
|
MetaDataType::BOOLEAN,
|
|
no_gravity,
|
|
)],
|
|
None,
|
|
);
|
|
}
|
|
|
|
pub fn send_velocity(&self) {
|
|
let velocity = self.velocity.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&CEntityVelocity::new(self.entity_id.into(), velocity),
|
|
&CSetActorMotion::new(
|
|
VarULong(self.entity_id as u64),
|
|
Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32),
|
|
VarULong(0),
|
|
),
|
|
);
|
|
}
|
|
|
|
#[must_use]
|
|
pub const fn get_entity_dimensions(pose: EntityPose) -> EntityDimensions {
|
|
match pose {
|
|
EntityPose::Sleeping => EntityDimensions::new(0.2, 0.2, 0.2),
|
|
EntityPose::FallFlying | EntityPose::Swimming | EntityPose::SpinAttack => {
|
|
EntityDimensions::new(0.6, 0.6, 0.4)
|
|
}
|
|
EntityPose::Crouching => EntityDimensions::new(0.6, 1.5, 1.27),
|
|
EntityPose::Dying => EntityDimensions::new(0.2, 0.2, 1.62),
|
|
_ => EntityDimensions::new(0.6, 1.8, 1.62),
|
|
}
|
|
}
|
|
|
|
pub fn get_eye_height(&self) -> f64 {
|
|
f64::from(Self::get_entity_dimensions(self.pose.load()).eye_height)
|
|
}
|
|
|
|
/// Updates the entity's position, block position, and chunk position.
|
|
///
|
|
/// This function calculates the new position, block position, and chunk position based on the provided coordinates. If any of these values change, the corresponding fields are updated.
|
|
pub fn set_pos(&self, new_position: Vector3<f64>) {
|
|
let pos = self.pos.load();
|
|
if pos != new_position {
|
|
self.pos.store(new_position);
|
|
self.bounding_box.store(BoundingBox::new_from_pos(
|
|
new_position.x,
|
|
new_position.y,
|
|
new_position.z,
|
|
&self.entity_dimension.load(),
|
|
));
|
|
|
|
let floor_x = new_position.x.floor() as i32;
|
|
let floor_y = new_position.y.floor() as i32;
|
|
let floor_z = new_position.z.floor() as i32;
|
|
|
|
let block_pos = self.block_pos.load();
|
|
let block_pos_vec = block_pos.0;
|
|
if floor_x != block_pos_vec.x
|
|
|| floor_y != block_pos_vec.y
|
|
|| floor_z != block_pos_vec.z
|
|
{
|
|
let new_block_pos = Vector3::new(floor_x, floor_y, floor_z);
|
|
self.block_pos.store(BlockPos(new_block_pos));
|
|
|
|
let chunk_pos = self.chunk_pos.load();
|
|
if get_section_cord(floor_x) != chunk_pos.x
|
|
|| get_section_cord(floor_z) != chunk_pos.y
|
|
{
|
|
self.chunk_pos.store(Vector2::new(
|
|
get_section_cord(new_block_pos.x),
|
|
get_section_cord(new_block_pos.z),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns entity rotation as vector
|
|
pub fn rotation(&self) -> Vector3<f32> {
|
|
let pitch_rad = self.pitch.load().to_radians();
|
|
let yaw_rad = -self.yaw.load().to_radians();
|
|
|
|
let cos_yaw = yaw_rad.cos();
|
|
let sin_yaw = yaw_rad.sin();
|
|
let cos_pitch = pitch_rad.cos();
|
|
let sin_pitch = pitch_rad.sin();
|
|
|
|
Vector3::new(sin_yaw * cos_pitch, -sin_pitch, cos_yaw * cos_pitch)
|
|
}
|
|
|
|
/// Changes this entity's pitch and yaw to look at target
|
|
pub fn look_at(&self, target: Vector3<f64>) {
|
|
let position = self.pos.load();
|
|
let delta = target.sub(&position);
|
|
let root = delta.x.hypot(delta.z);
|
|
let pitch = wrap_degrees((-delta.y.atan2(root) as f32).to_degrees());
|
|
let yaw = wrap_degrees((delta.z.atan2(delta.x) as f32).to_degrees() - 90.0);
|
|
self.pitch.store(pitch);
|
|
self.yaw.store(yaw);
|
|
}
|
|
|
|
pub fn send_rotation(&self) {
|
|
let yaw = self.yaw.load();
|
|
let pitch = self.pitch.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
|
|
// Broadcast the update packet.
|
|
|
|
let yaw = (yaw * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
|
let pitch = (pitch * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
|
|
|
if yaw == self.last_sent_yaw.load(Relaxed) && pitch == self.last_sent_pitch.load(Relaxed) {
|
|
return;
|
|
}
|
|
|
|
self.last_sent_yaw.store(yaw, Relaxed);
|
|
self.last_sent_pitch.store(pitch, Relaxed);
|
|
|
|
self.world.load().broadcast_to_chunk(
|
|
chunk_pos,
|
|
&CUpdateEntityRot::new(
|
|
self.entity_id.into(),
|
|
yaw,
|
|
pitch,
|
|
self.on_ground.load(Relaxed),
|
|
),
|
|
);
|
|
|
|
self.send_head_rot(yaw);
|
|
}
|
|
|
|
pub fn send_head_rot(&self, head_yaw: u8) {
|
|
let chunk_pos = self.chunk_pos.load();
|
|
if head_yaw == self.last_sent_head_yaw.load(Relaxed) {
|
|
return;
|
|
}
|
|
self.last_sent_head_yaw.store(head_yaw, Relaxed);
|
|
|
|
self.world
|
|
.load()
|
|
.broadcast_to_chunk(chunk_pos, &CHeadRot::new(self.entity_id.into(), head_yaw));
|
|
}
|
|
|
|
fn default_portal_cooldown(&self) -> u32 {
|
|
if self.entity_type == &EntityType::PLAYER {
|
|
10
|
|
} else {
|
|
300
|
|
}
|
|
}
|
|
|
|
/// Returns the block position of the block the (non-player) entity is standing on, if any.
|
|
pub fn get_supporting_block_pos(&self) -> Option<BlockPos> {
|
|
// Check if the entity is on the ground
|
|
if !self.on_ground.load(Ordering::Relaxed) {
|
|
return None;
|
|
}
|
|
|
|
self.supporting_block_pos.load()
|
|
}
|
|
|
|
#[expect(clippy::float_cmp)]
|
|
async fn adjust_movement_for_collisions(
|
|
&self,
|
|
movement: Vector3<f64>,
|
|
caller: &dyn EntityBase,
|
|
) -> Vector3<f64> {
|
|
if movement.length_squared() == 0.0 {
|
|
return movement;
|
|
}
|
|
|
|
self.on_ground.store(false, Ordering::SeqCst);
|
|
self.supporting_block_pos.store(None);
|
|
self.horizontal_collision.store(false, Ordering::SeqCst);
|
|
|
|
let bounding_box = self.bounding_box.load();
|
|
|
|
let (collisions, block_positions) = self
|
|
.world
|
|
.load()
|
|
.get_block_collisions(bounding_box.stretch(movement), caller)
|
|
.await;
|
|
|
|
if collisions.is_empty() {
|
|
return movement;
|
|
}
|
|
|
|
let mut adjusted_movement = movement;
|
|
|
|
// Y-Axis adjustment
|
|
if movement.get_axis(Axis::Y) != 0.0 {
|
|
let mut max_time = 1.0;
|
|
let mut positions = block_positions.into_iter();
|
|
if let Some((mut collisions_len, mut position)) = positions.next() {
|
|
let mut supporting_block_pos = None;
|
|
|
|
for (i, inert_box) in collisions.iter().enumerate() {
|
|
if i == collisions_len {
|
|
let Some((next_len, next_pos)) = positions.next() else {
|
|
break;
|
|
};
|
|
collisions_len = next_len;
|
|
position = next_pos;
|
|
}
|
|
|
|
if let Some(collision_time) = bounding_box.calculate_collision_time(
|
|
inert_box,
|
|
adjusted_movement,
|
|
Axis::Y,
|
|
max_time,
|
|
) {
|
|
max_time = collision_time;
|
|
|
|
// If the entity is moving downwards and collides, set the supporting block position
|
|
if movement.get_axis(Axis::Y) < 0.0 {
|
|
supporting_block_pos = Some(position);
|
|
}
|
|
}
|
|
}
|
|
|
|
if max_time != 1.0 {
|
|
let changed_component = adjusted_movement.get_axis(Axis::Y) * max_time;
|
|
adjusted_movement.set_axis(Axis::Y, changed_component);
|
|
}
|
|
|
|
self.on_ground
|
|
.store(supporting_block_pos.is_some(), Ordering::SeqCst);
|
|
self.supporting_block_pos.store(supporting_block_pos);
|
|
}
|
|
}
|
|
|
|
let mut horizontal_collision = false;
|
|
|
|
for axis in Axis::horizontal() {
|
|
if movement.get_axis(axis) == 0.0 {
|
|
continue;
|
|
}
|
|
|
|
let mut max_time = 1.0;
|
|
|
|
for inert_box in &collisions {
|
|
if let Some(collision_time) = bounding_box.calculate_collision_time(
|
|
inert_box,
|
|
adjusted_movement,
|
|
axis,
|
|
max_time,
|
|
) {
|
|
max_time = collision_time;
|
|
}
|
|
}
|
|
|
|
if max_time != 1.0 {
|
|
let changed_component = adjusted_movement.get_axis(axis) * max_time;
|
|
adjusted_movement.set_axis(axis, changed_component);
|
|
horizontal_collision = true;
|
|
}
|
|
}
|
|
|
|
self.horizontal_collision
|
|
.store(horizontal_collision, Ordering::SeqCst);
|
|
|
|
adjusted_movement
|
|
}
|
|
|
|
/// Applies knockback to the entity, following vanilla Minecraft's mechanics.
|
|
/// `LivingEntity.takeKnockback()`
|
|
/// This function calculates the entity's new velocity based on the specified knockback strength and direction.
|
|
///
|
|
/// Knockback resistance is not applied here, because it is a `LivingEntity`
|
|
/// attribute and this is an `Entity` method. Callers modelling vanilla's
|
|
/// `LivingEntity.knockback` scale `strength` with
|
|
/// `combat::knockback_after_resistance` first; callers modelling vanilla's raw
|
|
/// `Entity.push` (such as the ender dragon) pass `strength` unscaled.
|
|
pub fn apply_knockback(&self, strength: f64, mut x: f64, mut z: f64) {
|
|
if strength <= 0.0 {
|
|
return;
|
|
}
|
|
|
|
self.velocity_dirty.store(true, Ordering::SeqCst);
|
|
|
|
// This has some vanilla magic
|
|
|
|
while x.mul_add(x, z * z) < 1.0E-5 {
|
|
x = (rand::random::<f64>() - rand::random::<f64>()) * 0.01;
|
|
|
|
z = (rand::random::<f64>() - rand::random::<f64>()) * 0.01;
|
|
}
|
|
|
|
let var8 = Vector3::new(x, 0.0, z).normalize() * strength;
|
|
|
|
let velocity = self.velocity.load();
|
|
|
|
self.velocity.store(Vector3::new(
|
|
velocity.x / 2.0 - var8.x,
|
|
if self.on_ground.load(Relaxed) {
|
|
(velocity.y / 2.0 + strength).min(0.4)
|
|
} else {
|
|
velocity.y
|
|
},
|
|
velocity.z / 2.0 - var8.z,
|
|
));
|
|
}
|
|
|
|
// Part of LivingEntity.tickMovement() in yarn
|
|
|
|
pub fn check_zero_velo(&self) {
|
|
let mut motion = self.velocity.load();
|
|
|
|
if self.entity_type == &EntityType::PLAYER {
|
|
if motion.horizontal_length_squared() < 9.0E-6 {
|
|
motion.x = 0.0;
|
|
|
|
motion.z = 0.0;
|
|
}
|
|
} else {
|
|
if motion.x.abs() < 0.003 {
|
|
motion.x = 0.0;
|
|
}
|
|
|
|
if motion.z.abs() < 0.003 {
|
|
motion.z = 0.0;
|
|
}
|
|
}
|
|
|
|
if motion.y.abs() < 0.003 {
|
|
motion.y = 0.0;
|
|
}
|
|
|
|
self.velocity.store(motion);
|
|
}
|
|
|
|
#[expect(dead_code)]
|
|
fn tick_block_underneath(_caller: &Arc<dyn EntityBase>) {
|
|
// let world = self.world.read();
|
|
|
|
// let (pos, block, state) = self.get_block_with_y_offset(0.2);
|
|
|
|
// world
|
|
// .block_registry
|
|
// .on_stepped_on(&world, caller.as_ref(), pos, block, state)
|
|
// ;
|
|
|
|
// TODO: Add this to on_stepped_on
|
|
|
|
/*
|
|
|
|
|
|
if self.on_ground.load(Ordering::SeqCst) {
|
|
|
|
|
|
let (_pos, block, state) = self.get_block_with_y_offset(0.2);
|
|
|
|
|
|
if let Some(live) = living {
|
|
|
|
|
|
if block == Block::CAMPFIRE
|
|
|
|
|
|
|| block == Block::SOUL_CAMPFIRE
|
|
|
|
|
|
&& CampfireLikeProperties::from_state_id(state.id, &block).r#signal_fire
|
|
|
|
|
|
{
|
|
|
|
|
|
let _ = live.damage(1.0, DamageType::CAMPFIRE);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if block == Block::MAGMA_BLOCK {
|
|
|
|
|
|
let _ = live.damage(1.0, DamageType::HOT_FLOOR);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
*/
|
|
}
|
|
|
|
async fn tick_block_collisions(&self, caller: &Arc<dyn EntityBase>, server: &Server) -> bool {
|
|
let bounding_box = self.bounding_box.load();
|
|
let aabb = bounding_box.expand(-1.0e-7, -1.0e-7, -1.0e-7);
|
|
|
|
let min = aabb.min_block_pos();
|
|
let max = aabb.max_block_pos();
|
|
|
|
let eye_height = self.get_eye_height();
|
|
let mut eye_level_box = aabb;
|
|
eye_level_box.min.y += eye_height;
|
|
eye_level_box.max.y = eye_level_box.min.y;
|
|
|
|
let mut suffocating = false;
|
|
let world = self.world.load();
|
|
|
|
for pos in BlockPos::iterate(min, max) {
|
|
let (block, state) = world.get_block_and_state(&pos);
|
|
if state.is_air() {
|
|
continue;
|
|
}
|
|
|
|
// TODO: this is default predicate, vanilla overwrites it for some blocks,
|
|
// see .suffocates(...) in Blocks.java
|
|
let check_suffocation =
|
|
!suffocating && blocks_movement(state, block.id) && state.is_full_cube();
|
|
|
|
World::check_collision(
|
|
&bounding_box,
|
|
pos,
|
|
state,
|
|
check_suffocation,
|
|
|collision_shape: &BoundingBox| {
|
|
if collision_shape.intersects(&eye_level_box) {
|
|
suffocating = true;
|
|
}
|
|
},
|
|
);
|
|
|
|
let collision_shape = if block == &Block::POWDER_SNOW {
|
|
crate::block::blocks::powder_snow::inside_collision_shape_for_entity(
|
|
caller.as_ref(),
|
|
&pos,
|
|
)
|
|
.await
|
|
} else {
|
|
world
|
|
.block_registry
|
|
.get_inside_collision_shape(block, &world, state, &pos)
|
|
.await
|
|
};
|
|
|
|
if bounding_box.intersects(&collision_shape.at_pos(pos)) {
|
|
if block == &Block::POWDER_SNOW {
|
|
self.is_in_powder_snow.store(true, Relaxed);
|
|
}
|
|
world
|
|
.block_registry
|
|
.on_entity_collision(block, &world, caller.as_ref(), &pos, state, server)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
suffocating
|
|
}
|
|
|
|
#[expect(clippy::too_many_lines)]
|
|
pub fn send_pos_rot(&self) {
|
|
let old = self.last_sent_pos.load();
|
|
let new = self.pos.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
|
|
let converted = Vector3::new(
|
|
new.x.mul_add(4096.0, -(old.x * 4096.0)) as i16,
|
|
new.y.mul_add(4096.0, -(old.y * 4096.0)) as i16,
|
|
new.z.mul_add(4096.0, -(old.z * 4096.0)) as i16,
|
|
);
|
|
|
|
let yaw = self.yaw.load();
|
|
|
|
let pitch = self.pitch.load();
|
|
let yaw = (yaw * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
|
let pitch = (pitch * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
|
|
|
// Only broadcast when position or rotation has actually changed.
|
|
let pos_changed = converted.x != 0 || converted.y != 0 || converted.z != 0;
|
|
let rot_changed =
|
|
yaw != self.last_sent_yaw.load(Relaxed) || pitch != self.last_sent_pitch.load(Relaxed);
|
|
|
|
if !pos_changed && !rot_changed {
|
|
return;
|
|
}
|
|
|
|
self.last_sent_pos.store(new);
|
|
self.last_sent_yaw.store(yaw, Relaxed);
|
|
self.last_sent_pitch.store(pitch, Relaxed);
|
|
|
|
// Dynamically pick the most efficient packet
|
|
if pos_changed && rot_changed {
|
|
let je_packet = CUpdateEntityPosRot::new(
|
|
self.entity_id.into(),
|
|
Vector3::new(converted.x, converted.y, converted.z),
|
|
yaw,
|
|
pitch,
|
|
self.on_ground.load(Relaxed),
|
|
);
|
|
if self.entity_type == &EntityType::PLAYER {
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMovePlayer::new(
|
|
VarULong(self.entity_id as u64),
|
|
Vector3::new(new.x as f32, new.y as f32, new.z as f32),
|
|
self.pitch.load(),
|
|
self.yaw.load(),
|
|
self.yaw.load(),
|
|
CMovePlayer::MODE_NORMAL,
|
|
self.on_ground.load(Relaxed),
|
|
VarULong(0),
|
|
0,
|
|
0,
|
|
VarULong(0),
|
|
),
|
|
);
|
|
} else {
|
|
let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_X
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_Y
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_Z
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_PITCH
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_YAW
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW;
|
|
if self.on_ground.load(Relaxed) {
|
|
flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND;
|
|
}
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMoveActorDelta::new(
|
|
VarULong(self.entity_id as u64),
|
|
flags,
|
|
new.x as f32,
|
|
new.y as f32,
|
|
new.z as f32,
|
|
pitch,
|
|
yaw,
|
|
yaw,
|
|
),
|
|
);
|
|
}
|
|
} else if pos_changed {
|
|
let je_packet = CUpdateEntityPos::new(
|
|
self.entity_id.into(),
|
|
Vector3::new(converted.x, converted.y, converted.z),
|
|
self.on_ground.load(Relaxed),
|
|
);
|
|
if self.entity_type == &EntityType::PLAYER {
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMovePlayer::new(
|
|
VarULong(self.entity_id as u64),
|
|
Vector3::new(new.x as f32, new.y as f32, new.z as f32),
|
|
self.pitch.load(),
|
|
self.yaw.load(),
|
|
self.yaw.load(),
|
|
CMovePlayer::MODE_NORMAL,
|
|
self.on_ground.load(Relaxed),
|
|
VarULong(0),
|
|
0,
|
|
0,
|
|
VarULong(0),
|
|
),
|
|
);
|
|
} else {
|
|
let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_X
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_Y
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_Z;
|
|
if self.on_ground.load(Relaxed) {
|
|
flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND;
|
|
}
|
|
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMoveActorDelta::new(
|
|
VarULong(self.entity_id as u64),
|
|
flags,
|
|
new.x as f32,
|
|
new.y as f32,
|
|
new.z as f32,
|
|
0,
|
|
0,
|
|
0,
|
|
),
|
|
);
|
|
}
|
|
} else if rot_changed {
|
|
let je_packet = CUpdateEntityRot::new(
|
|
self.entity_id.into(),
|
|
yaw,
|
|
pitch,
|
|
self.on_ground.load(Relaxed),
|
|
);
|
|
if self.entity_type == &EntityType::PLAYER {
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMovePlayer::new(
|
|
VarULong(self.entity_id as u64),
|
|
Vector3::new(new.x as f32, new.y as f32, new.z as f32),
|
|
self.pitch.load(),
|
|
self.yaw.load(),
|
|
self.yaw.load(),
|
|
CMovePlayer::MODE_ROTATION,
|
|
self.on_ground.load(Relaxed),
|
|
VarULong(0),
|
|
0,
|
|
0,
|
|
VarULong(0),
|
|
),
|
|
);
|
|
} else {
|
|
let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_PITCH
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_YAW
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW;
|
|
if self.on_ground.load(Relaxed) {
|
|
flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND;
|
|
}
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMoveActorDelta::new(
|
|
VarULong(self.entity_id as u64),
|
|
flags,
|
|
new.x as f32,
|
|
new.y as f32,
|
|
new.z as f32,
|
|
pitch,
|
|
yaw,
|
|
yaw,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
self.send_head_rot(yaw);
|
|
}
|
|
|
|
pub fn send_bedrock_pos(&self) {
|
|
let position = self.pos.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
let mut flags =
|
|
MOVE_ACTOR_DELTA_FLAG_HAS_X | MOVE_ACTOR_DELTA_FLAG_HAS_Y | MOVE_ACTOR_DELTA_FLAG_HAS_Z;
|
|
if self.on_ground.load(Relaxed) {
|
|
flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND;
|
|
}
|
|
let packet = CMoveActorDelta::new(
|
|
VarULong(self.entity_id as u64),
|
|
flags,
|
|
position.x as f32,
|
|
position.y as f32,
|
|
position.z as f32,
|
|
0,
|
|
0,
|
|
0,
|
|
);
|
|
let world = self.world.load();
|
|
for player in world.players.load().iter() {
|
|
let center = player.get_entity().chunk_pos.load();
|
|
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32;
|
|
if is_within_view_distance(chunk_pos, center, view_distance)
|
|
&& let ClientPlatform::Bedrock(client) = player.client.as_ref()
|
|
{
|
|
client.try_enqueue_packet(&packet);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_last_pos(&self) -> Vector3<f64> {
|
|
let pos = self.pos.load();
|
|
let old = self.last_pos.load();
|
|
self.movement.store(pos - old);
|
|
self.last_pos.store(pos);
|
|
old
|
|
}
|
|
|
|
pub fn send_pos(&self) {
|
|
let old = self.last_sent_pos.load();
|
|
let new = self.pos.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
|
|
let converted = Vector3::new(
|
|
new.x.mul_add(4096.0, -(old.x * 4096.0)) as i16,
|
|
new.y.mul_add(4096.0, -(old.y * 4096.0)) as i16,
|
|
new.z.mul_add(4096.0, -(old.z * 4096.0)) as i16,
|
|
);
|
|
|
|
// Only broadcast when position has actually changed.
|
|
if converted.x == 0 && converted.y == 0 && converted.z == 0 {
|
|
return;
|
|
}
|
|
|
|
self.last_sent_pos.store(new);
|
|
|
|
let je_packet = CUpdateEntityPos::new(
|
|
self.entity_id.into(),
|
|
Vector3::new(converted.x, converted.y, converted.z),
|
|
self.on_ground.load(Relaxed),
|
|
);
|
|
|
|
if self.entity_type == &EntityType::PLAYER {
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMovePlayer::new(
|
|
VarULong(self.entity_id as u64),
|
|
Vector3::new(new.x as f32, new.y as f32, new.z as f32),
|
|
self.pitch.load(),
|
|
self.yaw.load(),
|
|
self.yaw.load(),
|
|
CMovePlayer::MODE_NORMAL,
|
|
self.on_ground.load(Relaxed),
|
|
VarULong(0),
|
|
0,
|
|
0,
|
|
VarULong(0),
|
|
),
|
|
);
|
|
} else {
|
|
let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_X
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_Y
|
|
| MOVE_ACTOR_DELTA_FLAG_HAS_Z;
|
|
if self.on_ground.load(Relaxed) {
|
|
flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND;
|
|
}
|
|
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
chunk_pos,
|
|
&je_packet,
|
|
&CMoveActorDelta::new(
|
|
VarULong(self.entity_id as u64),
|
|
flags,
|
|
new.x as f32,
|
|
new.y as f32,
|
|
new.z as f32,
|
|
0,
|
|
0,
|
|
0,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// updateWaterState() in yarn
|
|
|
|
async fn update_fluid_state(&self, caller: &Arc<dyn EntityBase>) {
|
|
let is_pushed = caller.is_pushed_by_fluids();
|
|
let mut fluids = BTreeMap::new();
|
|
|
|
let water_push = Vector3::default();
|
|
|
|
let water_n = 0;
|
|
|
|
let lava_push = Vector3::default();
|
|
|
|
let lava_n = 0;
|
|
|
|
let mut fluid_push = [water_push, lava_push];
|
|
|
|
let mut fluid_n = [water_n, lava_n];
|
|
|
|
let mut in_fluid = [false, false];
|
|
|
|
// The maximum fluid height found
|
|
|
|
let mut fluid_height: [f64; 2] = [0.0, 0.0];
|
|
|
|
let bounding_box = self.bounding_box.load().expand(-0.001, -0.001, -0.001);
|
|
|
|
let min = bounding_box.min_block_pos();
|
|
|
|
let max = bounding_box.max_block_pos();
|
|
|
|
let world = self.world.load();
|
|
|
|
for x in min.0.x..=max.0.x {
|
|
for y in min.0.y..=max.0.y {
|
|
for z in min.0.z..=max.0.z {
|
|
let pos = BlockPos::new(x, y, z);
|
|
|
|
let (fluid, state) = world.get_fluid_and_fluid_state(&pos);
|
|
|
|
if fluid.id != Fluid::EMPTY.id {
|
|
let marginal_height =
|
|
f64::from(state.height) + f64::from(y) - bounding_box.min.y;
|
|
|
|
if marginal_height >= 0.0 {
|
|
let i = usize::from(
|
|
fluid.id == Fluid::FLOWING_LAVA.id || fluid.id == Fluid::LAVA.id,
|
|
);
|
|
|
|
fluid_height[i] = fluid_height[i].max(marginal_height);
|
|
|
|
in_fluid[i] = true;
|
|
|
|
if !is_pushed {
|
|
fluids.insert(fluid.id, fluid);
|
|
|
|
continue;
|
|
}
|
|
|
|
let mut fluid_velo = world.get_fluid_velocity(pos, fluid, state);
|
|
|
|
if fluid_height[i] < 0.4 {
|
|
fluid_velo = fluid_velo * fluid_height[i];
|
|
}
|
|
|
|
fluid_push[i] += fluid_velo;
|
|
|
|
fluid_n[i] += 1;
|
|
|
|
fluids.insert(fluid.id, fluid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BTreeMap auto-sorts water before lava as in vanilla
|
|
|
|
for (_, fluid) in fluids {
|
|
world
|
|
.block_registry
|
|
.on_entity_collision_fluid(fluid, caller.as_ref())
|
|
.await;
|
|
}
|
|
|
|
let lava_speed = if world.dimension == Dimension::THE_NETHER {
|
|
0.007
|
|
} else {
|
|
0.002_333_333
|
|
};
|
|
|
|
self.push_by_fluid(0.014, fluid_push[0], fluid_n[0]);
|
|
|
|
self.push_by_fluid(lava_speed, fluid_push[1], fluid_n[1]);
|
|
|
|
let water_height = fluid_height[0];
|
|
|
|
let in_water = in_fluid[0];
|
|
|
|
if in_water {
|
|
if let Some(living) = caller.get_living_entity() {
|
|
living.fall_distance.store(0.0);
|
|
}
|
|
|
|
if !self.touching_water.load(Ordering::SeqCst) {
|
|
|
|
// TODO: Spawn splash particles
|
|
}
|
|
}
|
|
|
|
self.water_height.store(water_height);
|
|
|
|
self.touching_water.store(in_water, Ordering::SeqCst);
|
|
|
|
let lava_height = fluid_height[1];
|
|
|
|
let in_lava = in_fluid[1];
|
|
|
|
if in_lava && let Some(living) = caller.get_living_entity() {
|
|
let halved_fall = living.fall_distance.load() / 2.0;
|
|
|
|
if halved_fall != 0.0 {
|
|
living.fall_distance.store(halved_fall);
|
|
}
|
|
}
|
|
|
|
self.lava_height.store(lava_height);
|
|
|
|
self.touching_lava.store(in_lava, Ordering::SeqCst);
|
|
}
|
|
|
|
fn push_by_fluid(&self, speed: f64, mut push: Vector3<f64>, n: usize) {
|
|
if push.length_squared() != 0.0 {
|
|
if n > 0 {
|
|
push = push * (1.0 / (n as f64));
|
|
}
|
|
|
|
if self.entity_type != &EntityType::PLAYER {
|
|
push = push.normalize();
|
|
}
|
|
|
|
push = push * speed;
|
|
|
|
let velo = self.velocity.load();
|
|
|
|
if velo.x.abs() < 0.003 && velo.z.abs() < 0.003 && velo.length_squared() < 0.000_020_25
|
|
{
|
|
push = push.normalize() * 0.0045;
|
|
}
|
|
|
|
self.velocity.store(velo + push);
|
|
}
|
|
}
|
|
|
|
fn get_pos_with_y_offset(
|
|
&self,
|
|
offset: f64,
|
|
) -> (
|
|
BlockPos,
|
|
Option<&'static Block>,
|
|
Option<&'static BlockState>,
|
|
) {
|
|
if let Some(mut supporting_block) = self.supporting_block_pos.load() {
|
|
if offset > 1.0e-5 {
|
|
let (block, state) = self.world.load().get_block_and_state(&supporting_block);
|
|
|
|
// if let Some(props) = block.properties(state.id) {
|
|
// let name = props.;
|
|
|
|
// if offset <= 0.5
|
|
// && (name == "OakFenceLikeProperties"
|
|
// || name == "ResinBrickWallLikeProperties"
|
|
// || name == "OakFenceGateLikeProperties"
|
|
// && OakFenceGateLikeProperties::from_state_id(state.id, &block)
|
|
// .r#open)
|
|
// {
|
|
// return (supporting_block, Some(block), Some(state));
|
|
// }
|
|
// }
|
|
|
|
supporting_block.0.y = (self.pos.load().y - offset).floor() as i32;
|
|
|
|
return (supporting_block, Some(block), Some(state));
|
|
}
|
|
|
|
return (supporting_block, None, None);
|
|
}
|
|
|
|
let mut block_pos = self.block_pos.load();
|
|
|
|
block_pos.0.y = (self.pos.load().y - offset).floor() as i32;
|
|
|
|
(block_pos, None, None)
|
|
}
|
|
|
|
fn get_block_with_y_offset(
|
|
&self,
|
|
offset: f64,
|
|
) -> (BlockPos, &'static Block, &'static BlockState) {
|
|
let (pos, block, state) = self.get_pos_with_y_offset(offset);
|
|
|
|
if let (Some(b), Some(s)) = (block, state) {
|
|
(pos, b, s)
|
|
} else {
|
|
let (b, s) = self.world.load().get_block_and_state(&pos);
|
|
|
|
(pos, b, s)
|
|
}
|
|
}
|
|
|
|
// Entity.updateVelocity in yarn
|
|
|
|
fn update_velocity_from_input(&self, movement_input: Vector3<f64>, speed: f64) {
|
|
let final_input = self.movement_input_to_velocity(movement_input, speed);
|
|
|
|
self.velocity.store(self.velocity.load() + final_input);
|
|
}
|
|
|
|
// Entity.movementInputToVelocity in yarn
|
|
|
|
fn movement_input_to_velocity(&self, movement_input: Vector3<f64>, speed: f64) -> Vector3<f64> {
|
|
let yaw = f64::from(self.yaw.load()).to_radians();
|
|
|
|
let dist = movement_input.length_squared();
|
|
|
|
if dist < 1.0e-7 {
|
|
return Vector3::default();
|
|
}
|
|
|
|
let input = if dist > 1.0 {
|
|
movement_input.normalize() * speed
|
|
} else {
|
|
movement_input * speed
|
|
};
|
|
|
|
let sin = yaw.sin();
|
|
|
|
let cos = yaw.cos();
|
|
|
|
Vector3::new(
|
|
input.x.mul_add(cos, -(input.z * sin)),
|
|
input.y,
|
|
input.z.mul_add(cos, input.x * sin),
|
|
)
|
|
}
|
|
|
|
#[expect(clippy::float_cmp)]
|
|
fn get_velocity_multiplier(&self) -> f32 {
|
|
let block = self.world.load().get_block(&self.block_pos.load());
|
|
|
|
let multiplier = block.velocity_multiplier;
|
|
|
|
if multiplier != 1.0 || block == &Block::WATER || block == &Block::BUBBLE_COLUMN {
|
|
multiplier
|
|
} else {
|
|
let (_pos, block, _state) = self.get_block_with_y_offset(0.500_001);
|
|
|
|
block.velocity_multiplier
|
|
}
|
|
}
|
|
|
|
#[expect(clippy::float_cmp)]
|
|
fn get_jump_velocity_multiplier(&self) -> f32 {
|
|
let f = self
|
|
.world
|
|
.load()
|
|
.get_block(&self.block_pos.load())
|
|
.jump_velocity_multiplier;
|
|
|
|
let g = self
|
|
.get_block_with_y_offset(0.500_001)
|
|
.1
|
|
.jump_velocity_multiplier;
|
|
|
|
if f == 1f32 { g } else { f }
|
|
}
|
|
|
|
pub fn move_pos(&self, delta: Vector3<f64>) {
|
|
self.set_pos(self.pos.load() + delta);
|
|
}
|
|
|
|
// Move by a delta, adjust for collisions, and send
|
|
|
|
// Does not send movement. That must be done separately
|
|
pub async fn move_entity<'a>(
|
|
&'a self,
|
|
caller: &'a Arc<dyn EntityBase>,
|
|
mut motion: Vector3<f64>,
|
|
) {
|
|
if caller.get_player().is_some() {
|
|
return;
|
|
}
|
|
|
|
if self.no_clip.load(Ordering::Relaxed) {
|
|
self.move_pos(motion);
|
|
|
|
return;
|
|
}
|
|
|
|
let movement_multiplier = self.movement_multiplier.swap(Vector3::default());
|
|
|
|
if movement_multiplier.length_squared() > 1.0e-7 {
|
|
motion = motion.multiply(
|
|
movement_multiplier.x,
|
|
movement_multiplier.y,
|
|
movement_multiplier.z,
|
|
);
|
|
|
|
self.velocity.store(Vector3::default());
|
|
}
|
|
|
|
let final_move = self
|
|
.adjust_movement_for_collisions(motion, caller.as_ref())
|
|
.await;
|
|
|
|
self.move_pos(final_move);
|
|
|
|
let velocity_multiplier = f64::from(self.get_velocity_multiplier());
|
|
|
|
self.velocity.store(final_move * velocity_multiplier);
|
|
|
|
if let Some(living) = caller.get_living_entity() {
|
|
living
|
|
.fall(
|
|
caller.clone(),
|
|
final_move.y,
|
|
self.on_ground.load(Ordering::SeqCst),
|
|
false,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
if motion.y != final_move.y {
|
|
let world = self.world.load();
|
|
let block = self.get_block_with_y_offset(0.2).1;
|
|
world
|
|
.block_registry
|
|
.update_entity_movement_after_fall_on(block, caller.as_ref())
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub fn push_out_of_blocks(&self, center_pos: Vector3<f64>) {
|
|
let block_pos = BlockPos::floored_v(center_pos);
|
|
|
|
let delta = center_pos.sub(&block_pos.0.to_f64());
|
|
|
|
let mut min_dist = f64::MAX;
|
|
|
|
let mut direction = BlockDirection::Up;
|
|
|
|
for dir in BlockDirection::all() {
|
|
if dir == BlockDirection::Down {
|
|
continue;
|
|
}
|
|
|
|
let offset = dir.to_offset();
|
|
|
|
if self
|
|
.world
|
|
.load()
|
|
.get_block_state(&block_pos.offset(offset))
|
|
.is_full_cube()
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let component = delta.get_axis(dir.to_axis().into());
|
|
|
|
let dist = if dir.positive() {
|
|
1.0 - component
|
|
} else {
|
|
component
|
|
};
|
|
|
|
if dist < min_dist {
|
|
min_dist = dist;
|
|
|
|
direction = dir;
|
|
}
|
|
}
|
|
|
|
let amplitude = rand::random::<f64>().mul_add(0.2, 0.1);
|
|
|
|
let axis = direction.to_axis().into();
|
|
|
|
let sign = if direction.positive() { 1.0 } else { -1.0 };
|
|
|
|
let mut velo = self.velocity.load();
|
|
|
|
velo = velo * 0.75;
|
|
|
|
velo.set_axis(axis, sign * amplitude);
|
|
|
|
self.velocity.store(velo);
|
|
}
|
|
|
|
async fn tick_portal(&self, caller: &Arc<dyn EntityBase>) {
|
|
if self.portal_cooldown.load(Ordering::Relaxed) > 0 {
|
|
self.portal_cooldown.fetch_sub(1, Ordering::Relaxed);
|
|
}
|
|
let mut manager_guard = self.portal_manager.lock().await;
|
|
let mut should_remove = false;
|
|
if let Some(pmanager_mutex) = manager_guard.as_ref() {
|
|
let mut portal_processor = pmanager_mutex.lock().await;
|
|
if portal_processor.process_portal_teleportation(
|
|
&self.world.load(),
|
|
caller.as_ref(),
|
|
true,
|
|
) {
|
|
self.portal_cooldown
|
|
.store(self.default_portal_cooldown(), Ordering::Relaxed);
|
|
|
|
let transition = portal_processor
|
|
.portal_type
|
|
.get_portal_destination(
|
|
&self.world.load(),
|
|
portal_processor.destination_world.clone(),
|
|
caller,
|
|
portal_processor.entry_position,
|
|
portal_processor.source_portal.clone(),
|
|
)
|
|
.await;
|
|
|
|
drop(portal_processor);
|
|
|
|
if let Some(transition) = transition {
|
|
let dest_world = transition.new_world.clone();
|
|
let yaw = transition.yaw;
|
|
let pitch = transition.pitch;
|
|
let teleport_pos = transition.position;
|
|
|
|
// Teleport the main entity
|
|
caller
|
|
.clone()
|
|
.teleport(teleport_pos, yaw, pitch, dest_world.clone())
|
|
.await;
|
|
|
|
// Teleport all passengers recursively along with the vehicle
|
|
let yaw_delta = yaw.map(|y| y - self.yaw.load());
|
|
Self::teleport_passengers_recursive(self, teleport_pos, yaw_delta, &dest_world)
|
|
.await;
|
|
}
|
|
} else if portal_processor.portal_time == 0 {
|
|
should_remove = true;
|
|
}
|
|
}
|
|
if should_remove {
|
|
*manager_guard = None;
|
|
}
|
|
}
|
|
|
|
/// Recursively teleports all passengers (and their passengers) to the destination
|
|
fn teleport_passengers_recursive<'a>(
|
|
entity: &'a Self,
|
|
position: Vector3<f64>,
|
|
yaw_delta: Option<f32>,
|
|
dest_world: &'a Arc<World>,
|
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
|
|
Box::pin(async move {
|
|
let passengers = entity.passengers.lock().await.clone();
|
|
for passenger in passengers {
|
|
let passenger_entity = passenger.get_entity();
|
|
let passenger_yaw = yaw_delta.map(|delta| passenger_entity.yaw.load() + delta);
|
|
passenger_entity.portal_cooldown.store(
|
|
passenger_entity.default_portal_cooldown(),
|
|
Ordering::Relaxed,
|
|
);
|
|
|
|
// Get nested passengers before teleporting
|
|
let nested_passengers = passenger_entity.passengers.lock().await.clone();
|
|
|
|
passenger
|
|
.teleport(position, passenger_yaw, None, dest_world.clone())
|
|
.await;
|
|
|
|
// Recursively teleport nested passengers
|
|
for nested in nested_passengers {
|
|
let nested_entity = nested.get_entity();
|
|
Self::teleport_passengers_recursive(
|
|
nested_entity,
|
|
position,
|
|
yaw_delta,
|
|
dest_world,
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub async fn try_use_portal(
|
|
&self,
|
|
_portal_delay: u32,
|
|
portal_world: Arc<World>,
|
|
pos: BlockPos,
|
|
) {
|
|
// Passengers don't teleport independently - they wait for their vehicle
|
|
if self.has_vehicle().await {
|
|
return;
|
|
}
|
|
|
|
if self.portal_cooldown.load(Ordering::Relaxed) > 0 {
|
|
self.portal_cooldown
|
|
.store(self.default_portal_cooldown(), Ordering::Relaxed);
|
|
return;
|
|
}
|
|
|
|
let Some(server) = portal_world.server.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
if (portal_world.dimension == Dimension::THE_NETHER && !server.basic_config.allow_nether)
|
|
|| (portal_world.dimension == Dimension::THE_END && !server.basic_config.allow_end)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let mut manager = self.portal_manager.lock().await;
|
|
let world = self.world.load();
|
|
if manager.is_none() {
|
|
let portal_type = if portal_world.dimension == Dimension::THE_END
|
|
|| self.world.load().dimension == Dimension::THE_END
|
|
{
|
|
PortalType::End
|
|
} else {
|
|
PortalType::Nether
|
|
};
|
|
|
|
let mut new_manager = PortalProcessor::new(portal_type, pos, portal_world);
|
|
|
|
if let Some(portal) = NetherPortal::get_on_axis(
|
|
&world,
|
|
&pos,
|
|
pumpkin_data::block_properties::HorizontalAxis::X,
|
|
) && portal.was_already_valid()
|
|
{
|
|
new_manager.set_source_portal(SourcePortalInfo {
|
|
lower_corner: portal.lower_corner(),
|
|
axis: portal.axis(),
|
|
width: portal.width(),
|
|
height: portal.height(),
|
|
});
|
|
} else if let Some(portal) = NetherPortal::get_on_axis(
|
|
&world,
|
|
&pos,
|
|
pumpkin_data::block_properties::HorizontalAxis::Z,
|
|
) && portal.was_already_valid()
|
|
{
|
|
new_manager.set_source_portal(SourcePortalInfo {
|
|
lower_corner: portal.lower_corner(),
|
|
axis: portal.axis(),
|
|
width: portal.width(),
|
|
height: portal.height(),
|
|
});
|
|
}
|
|
|
|
*manager = Some(Mutex::new(new_manager));
|
|
} else if let Some(manager) = manager.as_ref() {
|
|
let mut manager = manager.lock().await;
|
|
manager.entry_position = pos;
|
|
manager.inside_portal_this_tick = true;
|
|
}
|
|
}
|
|
|
|
/// Extinguishes this entity.
|
|
pub fn extinguish(&self) {
|
|
self.fire_ticks.store(0, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Maximum freeze ticks (7 seconds at 20 tps)
|
|
pub const MAX_FROZEN_TICKS: i32 = 140;
|
|
|
|
/// Freeze damage is dealt every 40 ticks when fully frozen
|
|
const FREEZE_DAMAGE_INTERVAL: i32 = 40;
|
|
|
|
/// Check if the entity is currently in powder snow.
|
|
///
|
|
/// The flag is reset at the start of each tick and set while processing
|
|
/// block collisions for the current tick.
|
|
pub fn is_in_powder_snow(&self) -> bool {
|
|
self.is_in_powder_snow.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Check if this entity type is immune to freezing
|
|
pub fn is_freeze_immune(&self) -> bool {
|
|
self.entity_type
|
|
.has_tag(&tag::EntityType::MINECRAFT_FREEZE_IMMUNE_ENTITY_TYPES)
|
|
}
|
|
|
|
/// Mirrors vanilla `LivingEntity#canFreeze`: spectators and entities wearing
|
|
/// freeze-immune wearables (e.g. leather armor) cannot freeze.
|
|
async fn can_freeze(&self, caller: &dyn EntityBase) -> bool {
|
|
if caller.is_spectator() || self.is_freeze_immune() {
|
|
return false;
|
|
}
|
|
|
|
let Some(living) = caller.get_living_entity() else {
|
|
return true;
|
|
};
|
|
|
|
let equipment = living.entity_equipment.lock().await;
|
|
for (slot, stack) in &equipment.equipment {
|
|
if (*slot == EquipmentSlot::HEAD
|
|
|| *slot == EquipmentSlot::CHEST
|
|
|| *slot == EquipmentSlot::LEGS
|
|
|| *slot == EquipmentSlot::FEET)
|
|
&& stack
|
|
.get_item()
|
|
.has_tag(&tag::Item::MINECRAFT_FREEZE_IMMUNE_WEARABLES)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Ticks the frozen state of the entity.
|
|
/// In powder snow and freezeable: `frozen_ticks` increases by 1 (up to `MAX_FROZEN_TICKS`)
|
|
/// Otherwise: `frozen_ticks` decreases by 2 (down to 0)
|
|
/// When fully frozen, deals 1 damage every 40 ticks
|
|
pub async fn tick_frozen(&self, caller: &dyn EntityBase) {
|
|
let can_freeze = self.can_freeze(caller).await;
|
|
let in_powder_snow = self.is_in_powder_snow();
|
|
let old_frozen_ticks = self.frozen_ticks.load(Ordering::Relaxed);
|
|
|
|
let new_frozen_ticks = if in_powder_snow && can_freeze {
|
|
// Increase frozen ticks when in powder snow
|
|
(old_frozen_ticks + 1).min(Self::MAX_FROZEN_TICKS)
|
|
} else {
|
|
// Vanilla: thaw whenever not in powder snow OR when freezing is prevented
|
|
(old_frozen_ticks - 2).max(0)
|
|
};
|
|
|
|
// Only update and send metadata if the value changed
|
|
if new_frozen_ticks != old_frozen_ticks {
|
|
self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed);
|
|
let mut bedrock_meta = EntityMetadata::new();
|
|
bedrock_meta.set(
|
|
entity_data_key::FREEZING_EFFECT_STRENGTH,
|
|
MetadataValue::Float(new_frozen_ticks as f32),
|
|
);
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::TICKS_FROZEN,
|
|
MetaDataType::INTEGER,
|
|
VarInt(new_frozen_ticks),
|
|
)],
|
|
Some(&bedrock_meta),
|
|
);
|
|
}
|
|
|
|
// Vanilla parity: full-freeze damage is tick-phase based.
|
|
if can_freeze
|
|
&& new_frozen_ticks >= Self::MAX_FROZEN_TICKS
|
|
&& self.age.load(Ordering::Relaxed) % Self::FREEZE_DAMAGE_INTERVAL == 0
|
|
{
|
|
caller.damage(caller, 1.0, DamageType::FREEZE).await;
|
|
}
|
|
}
|
|
|
|
/// Sets the `Entity` yaw & pitch rotation
|
|
pub fn set_rotation(&self, yaw: f32, pitch: f32) {
|
|
// TODO
|
|
self.yaw.store(yaw);
|
|
self.set_pitch(pitch);
|
|
}
|
|
|
|
pub fn set_pitch(&self, pitch: f32) {
|
|
self.pitch.store(pitch.clamp(-90.0, 90.0) % 360.0);
|
|
}
|
|
|
|
/// Removes the `Entity` from their current `World`
|
|
pub async fn remove(&self) {
|
|
self.world.load().remove_entity(self).await;
|
|
}
|
|
|
|
pub fn create_spawn_packet(&self) -> CSpawnEntity {
|
|
let entity_loc = self.pos.load();
|
|
let entity_vel = self.velocity.load();
|
|
CSpawnEntity::new(
|
|
VarInt(self.entity_id),
|
|
self.entity_uuid,
|
|
VarInt(i32::from(self.entity_type.id)),
|
|
entity_loc,
|
|
self.pitch.load(),
|
|
self.yaw.load(),
|
|
self.head_yaw.load(), // todo: head_yaw and yaw are swapped, find out why
|
|
self.data.load(Relaxed).into(),
|
|
entity_vel,
|
|
)
|
|
}
|
|
pub fn width(&self) -> f32 {
|
|
self.entity_dimension.load().width
|
|
}
|
|
|
|
pub fn height(&self) -> f32 {
|
|
self.entity_dimension.load().height
|
|
}
|
|
|
|
/// Applies knockback to the entity, following vanilla Minecraft's mechanics.
|
|
///
|
|
/// This function calculates the entity's new velocity based on the specified knockback strength and direction.
|
|
pub fn knockback(&self, strength: f64, x: f64, z: f64) {
|
|
// This has some vanilla magic
|
|
let mut x = x;
|
|
let mut z = z;
|
|
while x.mul_add(x, z * z) < 1.0E-5 {
|
|
x = (rand::random::<f64>() - rand::random::<f64>()) * 0.01;
|
|
z = (rand::random::<f64>() - rand::random::<f64>()) * 0.01;
|
|
}
|
|
|
|
let var8 = Vector3::new(x, 0.0, z).normalize() * strength;
|
|
let velocity = self.velocity.load();
|
|
self.velocity.store(Vector3::new(
|
|
velocity.x / 2.0 - var8.x,
|
|
if self.on_ground.load(Relaxed) {
|
|
(velocity.y / 2.0 + strength).min(0.4)
|
|
} else {
|
|
velocity.y
|
|
},
|
|
velocity.z / 2.0 - var8.z,
|
|
));
|
|
}
|
|
|
|
pub async fn set_sneaking(&self, sneaking: bool) {
|
|
//assert!(self.sneaking.load(Relaxed) != sneaking);
|
|
self.sneaking.store(sneaking, Relaxed);
|
|
self.set_flag(Flag::Sneaking, sneaking).await;
|
|
}
|
|
pub fn is_sneaking(&self) -> bool {
|
|
self.sneaking.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub async fn set_swimming(&self, invisible: bool) {
|
|
if self.swimming.load(Ordering::Relaxed) != invisible {
|
|
self.swimming.store(invisible, Relaxed);
|
|
self.set_flag(Flag::Swimming, invisible).await;
|
|
}
|
|
}
|
|
|
|
/// Sets whether the entity is invisible and sends updated metadata.
|
|
pub async fn set_invisible(&self, invisible: bool) {
|
|
if self.invisible.load(Ordering::Relaxed) != invisible {
|
|
self.invisible.store(invisible, Relaxed);
|
|
self.set_flag(Flag::Invisible, invisible).await;
|
|
}
|
|
}
|
|
|
|
/// Sets whether the entity is glowing and sends updated metadata.
|
|
pub async fn set_glowing(&self, glowing: bool) {
|
|
if self.glowing.load(Ordering::Relaxed) != glowing {
|
|
self.glowing.store(glowing, Ordering::Relaxed);
|
|
self.set_flag(Flag::Glowing, glowing).await;
|
|
}
|
|
}
|
|
|
|
/// Sets whether the entity is on fire for visual and damage purposes. This is separate from `fire_ticks` which tracks the damage aspect of being on fire.
|
|
pub async fn set_on_fire(&self, on_fire: bool) {
|
|
if self.has_visual_fire.load(Ordering::Relaxed) != on_fire {
|
|
self.has_visual_fire.store(on_fire, Ordering::Relaxed);
|
|
self.set_flag(Flag::OnFire, on_fire).await;
|
|
}
|
|
}
|
|
|
|
pub fn get_horizontal_facing(&self) -> HorizontalFacing {
|
|
let yaw = self.yaw.load();
|
|
// Use vanilla's formula: floor(angle / 90.0 + 0.5) & 3
|
|
let quarter_turns = ((yaw / 90.0) + 0.5).floor() as i32 & 3;
|
|
match quarter_turns {
|
|
0 => HorizontalFacing::South,
|
|
1 => HorizontalFacing::West,
|
|
2 => HorizontalFacing::North,
|
|
_ => HorizontalFacing::East,
|
|
}
|
|
}
|
|
|
|
pub fn get_rotation_16(&self) -> u8 {
|
|
let adjusted_yaw = self.yaw.load().rem_euclid(360.0);
|
|
|
|
((adjusted_yaw / 22.5).round() as u8) % 16
|
|
}
|
|
|
|
pub fn get_flipped_rotation_16(&self) -> u8 {
|
|
(self.get_rotation_16() + 8) % 16
|
|
}
|
|
|
|
pub fn get_facing(&self) -> Facing {
|
|
let pitch = self.pitch.load().to_radians();
|
|
let yaw = -self.yaw.load().to_radians();
|
|
|
|
let (sin_p, cos_p) = pitch.sin_cos();
|
|
let (sin_y, cos_y) = yaw.sin_cos();
|
|
|
|
let x = sin_y * cos_p;
|
|
let y = -sin_p;
|
|
let z = cos_y * cos_p;
|
|
|
|
let ax = x.abs();
|
|
let ay = y.abs();
|
|
let az = z.abs();
|
|
|
|
if ax > ay && ax > az {
|
|
if x > 0.0 { Facing::East } else { Facing::West }
|
|
} else if ay > ax && ay > az {
|
|
if y > 0.0 { Facing::Up } else { Facing::Down }
|
|
} else if z > 0.0 {
|
|
Facing::South
|
|
} else {
|
|
Facing::North
|
|
}
|
|
}
|
|
|
|
pub fn get_entity_facing_order(&self) -> [Facing; 6] {
|
|
let pitch = self.pitch.load().to_radians();
|
|
let yaw = -self.yaw.load().to_radians();
|
|
|
|
let sin_p = pitch.sin();
|
|
let cos_p = pitch.cos();
|
|
let sin_y = yaw.sin();
|
|
let cos_y = yaw.cos();
|
|
|
|
let east_west = if sin_y > 0.0 {
|
|
Facing::East
|
|
} else {
|
|
Facing::West
|
|
};
|
|
let up_down = if sin_p < 0.0 {
|
|
Facing::Up
|
|
} else {
|
|
Facing::Down
|
|
};
|
|
let south_north = if cos_y > 0.0 {
|
|
Facing::South
|
|
} else {
|
|
Facing::North
|
|
};
|
|
|
|
let x_axis = sin_y.abs();
|
|
let y_axis = sin_p.abs();
|
|
let z_axis = cos_y.abs();
|
|
let x_weight = x_axis * cos_p;
|
|
let z_weight = z_axis * cos_p;
|
|
|
|
let (first, second, third) = if x_axis > z_axis {
|
|
if y_axis > x_weight {
|
|
(up_down, east_west, south_north)
|
|
} else if z_weight > y_axis {
|
|
(east_west, south_north, up_down)
|
|
} else {
|
|
(east_west, up_down, south_north)
|
|
}
|
|
} else if y_axis > z_weight {
|
|
(up_down, south_north, east_west)
|
|
} else if x_weight > y_axis {
|
|
(south_north, east_west, up_down)
|
|
} else {
|
|
(south_north, up_down, east_west)
|
|
};
|
|
|
|
[
|
|
first,
|
|
second,
|
|
third,
|
|
third.opposite(),
|
|
second.opposite(),
|
|
first.opposite(),
|
|
]
|
|
}
|
|
|
|
pub async fn set_sprinting(&self, sprinting: bool) {
|
|
//assert!(self.sprinting.load(Relaxed) != sprinting);
|
|
self.sprinting.store(sprinting, Relaxed);
|
|
self.set_flag(Flag::Sprinting, sprinting).await;
|
|
}
|
|
|
|
pub fn is_sprinting(&self) -> bool {
|
|
self.sprinting.load(Ordering::Relaxed)
|
|
}
|
|
pub fn check_fall_flying(&self) -> bool {
|
|
!self.on_ground.load(Relaxed)
|
|
}
|
|
|
|
pub async fn set_fall_flying(&self, fall_flying: bool) {
|
|
assert_ne!(self.fall_flying.load(Relaxed), fall_flying);
|
|
self.fall_flying.store(fall_flying, Relaxed);
|
|
self.set_flag(Flag::FallFlying, fall_flying).await;
|
|
}
|
|
pub fn is_fall_flying(&self) -> bool {
|
|
self.fall_flying.load(Ordering::Relaxed)
|
|
}
|
|
|
|
async fn set_flag(&self, flag: Flag, value: bool) {
|
|
let index = flag as u8;
|
|
let mask = (1i8).wrapping_shl(index as u32);
|
|
let new_je_flags = if value {
|
|
self.flags.fetch_or(mask, Ordering::Relaxed) | mask
|
|
} else {
|
|
self.flags.fetch_and(!mask, Ordering::Relaxed) & !mask
|
|
};
|
|
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::SHARED_FLAGS_ID,
|
|
MetaDataType::BYTE,
|
|
new_je_flags,
|
|
)],
|
|
None,
|
|
);
|
|
|
|
if let Some(bedrock_flag) = flag.to_bedrock() {
|
|
let (key, index) = if bedrock_flag >= 64 {
|
|
(entity_data_key::FLAGS_TWO, (bedrock_flag - 64) as u8)
|
|
} else {
|
|
(entity_data_key::FLAGS, bedrock_flag as u8)
|
|
};
|
|
|
|
if value {
|
|
let mask = 1i64 << index;
|
|
if key == entity_data_key::FLAGS {
|
|
self.bedrock_flags.fetch_or(mask, Ordering::Relaxed);
|
|
} else {
|
|
self.bedrock_flags_two.fetch_or(mask, Ordering::Relaxed);
|
|
}
|
|
} else {
|
|
let mask = !(1i64 << index);
|
|
if key == entity_data_key::FLAGS {
|
|
self.bedrock_flags.fetch_and(mask, Ordering::Relaxed);
|
|
} else {
|
|
self.bedrock_flags_two.fetch_and(mask, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
let world = self.world.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
for player in world.players.load().iter() {
|
|
if let ClientPlatform::Bedrock(client) = player.client.as_ref() {
|
|
let center = player.get_entity().chunk_pos.load();
|
|
let view_distance =
|
|
crate::world::chunker::get_view_distance(player).get() as i32;
|
|
|
|
if is_within_view_distance(chunk_pos, center, view_distance) {
|
|
let mut metadata = EntityMetadata(std::collections::HashMap::new());
|
|
metadata.set(
|
|
entity_data_key::FLAGS,
|
|
MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)),
|
|
);
|
|
metadata.set(
|
|
entity_data_key::FLAGS_TWO,
|
|
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
|
|
);
|
|
client
|
|
.enqueue_packet(&CSetActorData {
|
|
actor_runtime_id: VarULong(self.entity_id as u64),
|
|
metadata,
|
|
synced_properties: PropertySyncData {
|
|
int_properties: std::collections::HashMap::new(),
|
|
float_properties: std::collections::HashMap::new(),
|
|
},
|
|
tick: VarULong(0),
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Plays sound at this entity's position with the entity's sound category
|
|
pub fn play_sound(&self, sound: Sound) {
|
|
self.world
|
|
.load()
|
|
.play_sound(sound, SoundCategory::Neutral, &self.pos.load());
|
|
}
|
|
|
|
pub fn send_meta_data<T: MetadataSerializer>(
|
|
&self,
|
|
meta: &[Metadata<T>],
|
|
bedrock_meta: Option<&EntityMetadata>,
|
|
) {
|
|
let world = self.world.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
|
|
for player in world.players.load().iter() {
|
|
match player.client.as_ref() {
|
|
ClientPlatform::Java(client) => {
|
|
// Apply Chebyshev distance check
|
|
let center = player.get_entity().chunk_pos.load();
|
|
let view_distance =
|
|
crate::world::chunker::get_view_distance(player).get() as i32;
|
|
|
|
if is_within_view_distance(chunk_pos, center, view_distance) {
|
|
let mut buf = Vec::new();
|
|
for m in meta {
|
|
let _ = m.write(&mut buf, &client.version.load());
|
|
}
|
|
buf.put_u8(255);
|
|
player.client.try_enqueue_packet(&CSetEntityMetadata::new(
|
|
self.entity_id.into(),
|
|
buf.into(),
|
|
));
|
|
}
|
|
}
|
|
ClientPlatform::Bedrock(client) => {
|
|
if let Some(bedrock_meta) = bedrock_meta {
|
|
let center = player.get_entity().chunk_pos.load();
|
|
let view_distance =
|
|
crate::world::chunker::get_view_distance(player).get() as i32;
|
|
|
|
if is_within_view_distance(chunk_pos, center, view_distance) {
|
|
client.try_enqueue_packet(&CSetActorData {
|
|
actor_runtime_id: VarULong(self.entity_id as u64),
|
|
metadata: EntityMetadata(bedrock_meta.0.clone()),
|
|
synced_properties: PropertySyncData {
|
|
int_properties: std::collections::HashMap::new(),
|
|
float_properties: std::collections::HashMap::new(),
|
|
},
|
|
tick: VarULong(0),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_pose(&self, pose: EntityPose) {
|
|
let dimension = Self::get_entity_dimensions(pose);
|
|
let position = self.pos.load();
|
|
let aabb = BoundingBox::new_from_pos(position.x, position.y, position.z, &dimension);
|
|
if self.world.load().is_space_empty(aabb.contract_all(1.0E-7)) {
|
|
self.pose.store(pose);
|
|
let dimension = Self::get_entity_dimensions(pose);
|
|
self.bounding_box.store(aabb);
|
|
self.entity_dimension.store(dimension);
|
|
let pose = pose as i32;
|
|
let mut bedrock_meta = EntityMetadata::new();
|
|
bedrock_meta.set(entity_data_key::POSE_INDEX, MetadataValue::Int(pose));
|
|
self.send_meta_data(
|
|
&[Metadata::new(
|
|
TrackedData::POSE,
|
|
MetaDataType::ENTITY_POSE,
|
|
VarInt(pose),
|
|
)],
|
|
Some(&bedrock_meta),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Checks if the entity is invulnerable to the given damage type, considering both general invulnerability and specific immunities.
|
|
pub async fn is_invulnerable_to(&self, damage_type: &DamageType) -> bool {
|
|
// Nothing is immune to void or kill
|
|
if matches!(
|
|
*damage_type,
|
|
DamageType::GENERIC_KILL | DamageType::OUT_OF_WORLD
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// General invulnerability
|
|
if self.invulnerable.load(Ordering::Relaxed) {
|
|
return true;
|
|
}
|
|
|
|
// Specific type immunities
|
|
self.damage_immunities.lock().await.contains(damage_type)
|
|
}
|
|
|
|
/// Sets if the entity is invulnerable to a specific damage type
|
|
pub async fn set_damage_immunity(&self, damage_type: DamageType, immune: bool) {
|
|
let mut immunities = self.damage_immunities.lock().await;
|
|
if immune {
|
|
if !immunities.contains(&damage_type) {
|
|
immunities.push(damage_type);
|
|
}
|
|
} else {
|
|
// retain is cleaner than finding index and removing
|
|
immunities.retain(|dt| dt != &damage_type);
|
|
}
|
|
}
|
|
|
|
/// Sets if the entity is invulnerable to all damage types (except `GENERIC_KILL` and `OUT_OF_WORLD`)
|
|
pub fn set_invulnerable(&self, invulnerable: bool) {
|
|
self.invulnerable.store(invulnerable, Relaxed);
|
|
}
|
|
|
|
pub async fn check_block_collision(entity: &dyn EntityBase, server: &Server) {
|
|
let aabb = entity.get_entity().bounding_box.load();
|
|
let blockpos = BlockPos::new(
|
|
(aabb.min.x + 0.001).floor() as i32,
|
|
(aabb.min.y + 0.001).floor() as i32,
|
|
(aabb.min.z + 0.001).floor() as i32,
|
|
);
|
|
let blockpos1 = BlockPos::new(
|
|
(aabb.max.x - 0.001).floor() as i32,
|
|
(aabb.max.y - 0.001).floor() as i32,
|
|
(aabb.max.z - 0.001).floor() as i32,
|
|
);
|
|
let world = entity.get_entity().world.load();
|
|
|
|
for x in blockpos.0.x..=blockpos1.0.x {
|
|
for y in blockpos.0.y..=blockpos1.0.y {
|
|
for z in blockpos.0.z..=blockpos1.0.z {
|
|
let pos = BlockPos::new(x, y, z);
|
|
let (block, state) = world.get_block_and_state(&pos);
|
|
let block_outlines = state.get_block_outline_shapes();
|
|
|
|
if state.outline_shapes.is_empty() {
|
|
world
|
|
.block_registry
|
|
.on_entity_collision(block, &world, entity, &pos, state, server)
|
|
.await;
|
|
let fluid = world.get_fluid(&pos);
|
|
world
|
|
.block_registry
|
|
.on_entity_collision_fluid(fluid, entity)
|
|
.await;
|
|
continue;
|
|
}
|
|
for outline in block_outlines {
|
|
let outline_aabb = outline.at_pos(pos);
|
|
if outline_aabb.intersects(&aabb) {
|
|
world
|
|
.block_registry
|
|
.on_entity_collision(block, &world, entity, &pos, state, server)
|
|
.await;
|
|
let fluid = world.get_fluid(&pos);
|
|
world
|
|
.block_registry
|
|
.on_entity_collision_fluid(fluid, entity)
|
|
.await;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn teleport(
|
|
&self,
|
|
position: Vector3<f64>,
|
|
yaw: Option<f32>,
|
|
pitch: Option<f32>,
|
|
_world: Arc<World>,
|
|
) {
|
|
// Update server-side position and bounding box
|
|
self.set_pos(position);
|
|
if let Some(yaw) = yaw {
|
|
self.yaw.store(yaw);
|
|
}
|
|
if let Some(pitch) = pitch {
|
|
self.set_pitch(pitch);
|
|
}
|
|
// Update cache so we don't send rubberbanding deltas
|
|
self.last_sent_pos.store(position);
|
|
if let Some(yaw) = yaw {
|
|
self.last_sent_yaw
|
|
.store((yaw * 256.0 / 360.0).rem_euclid(256.0) as u8, Relaxed);
|
|
self.last_sent_head_yaw
|
|
.store((yaw * 256.0 / 360.0).rem_euclid(256.0) as u8, Relaxed);
|
|
}
|
|
if let Some(pitch) = pitch {
|
|
self.last_sent_pitch
|
|
.store((pitch * 256.0 / 360.0).rem_euclid(256.0) as u8, Relaxed);
|
|
}
|
|
let chunk_pos = self.chunk_pos.load();
|
|
self.world.load().broadcast_to_chunk(
|
|
chunk_pos,
|
|
&CEntityPositionSync::new(
|
|
self.entity_id.into(),
|
|
position,
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
yaw.unwrap_or(self.yaw.load()),
|
|
pitch.unwrap_or(self.pitch.load()),
|
|
self.on_ground.load(Ordering::SeqCst),
|
|
),
|
|
);
|
|
}
|
|
|
|
pub fn get_eye_pos(&self) -> Vector3<f64> {
|
|
let pos = self.pos.load();
|
|
Vector3::new(
|
|
pos.x,
|
|
pos.y + f64::from(self.entity_dimension.load().eye_height),
|
|
pos.z,
|
|
)
|
|
}
|
|
|
|
pub fn get_eye_y(&self) -> f64 {
|
|
self.pos.load().y + f64::from(self.entity_dimension.load().eye_height)
|
|
}
|
|
|
|
pub fn is_removed(&self) -> bool {
|
|
self.removal_reason.load().is_some()
|
|
}
|
|
|
|
pub fn is_alive(&self) -> bool {
|
|
!self.is_removed()
|
|
}
|
|
|
|
pub const LEASH_SNAP_DISTANCE: f64 = 12.0;
|
|
pub const LEASH_ELASTIC_DISTANCE: f64 = 6.0;
|
|
|
|
pub async fn leash_to(&self, holder: Arc<dyn EntityBase>) {
|
|
let holder_entity = holder.get_entity();
|
|
*self.leashed_to.lock().await = Some(holder.clone());
|
|
|
|
let je_packet = pumpkin_protocol::java::client::play::CSetEntityLink::new(
|
|
self.entity_id,
|
|
holder_entity.entity_id,
|
|
);
|
|
let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink {
|
|
link: pumpkin_protocol::bedrock::client::common::EntityLink {
|
|
ridden_unique_id: pumpkin_protocol::codec::var_long::VarLong(self.entity_id as i64),
|
|
rider_unique_id: pumpkin_protocol::codec::var_long::VarLong(
|
|
holder_entity.entity_id as i64,
|
|
),
|
|
link_type: 1, // Leash link
|
|
immediate: true,
|
|
rider_initiated: false,
|
|
vehicle_angular_velocity: 0.0,
|
|
},
|
|
};
|
|
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
self.chunk_pos.load(),
|
|
&je_packet,
|
|
&be_packet,
|
|
);
|
|
}
|
|
|
|
pub async fn unleash(&self) {
|
|
let old_holder = self.leashed_to.lock().await.take();
|
|
if old_holder.is_none() {
|
|
return;
|
|
}
|
|
|
|
let je_packet =
|
|
pumpkin_protocol::java::client::play::CSetEntityLink::new(self.entity_id, -1);
|
|
let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink {
|
|
link: pumpkin_protocol::bedrock::client::common::EntityLink {
|
|
ridden_unique_id: pumpkin_protocol::codec::var_long::VarLong(self.entity_id as i64),
|
|
rider_unique_id: pumpkin_protocol::codec::var_long::VarLong(-1),
|
|
link_type: 0, // Unlink
|
|
immediate: true,
|
|
rider_initiated: false,
|
|
vehicle_angular_velocity: 0.0,
|
|
},
|
|
};
|
|
|
|
self.world.load().broadcast_to_chunk_editioned_sync(
|
|
self.chunk_pos.load(),
|
|
&je_packet,
|
|
&be_packet,
|
|
);
|
|
}
|
|
|
|
pub async fn tick_leash(&self) {
|
|
let holder = {
|
|
let guard = self.leashed_to.lock().await;
|
|
guard.clone()
|
|
};
|
|
|
|
if let Some(holder) = holder {
|
|
let holder_entity = holder.get_entity();
|
|
|
|
// Drop leash if entity or holder is removed or dead
|
|
if !self.is_alive() || !holder_entity.is_alive() {
|
|
self.unleash().await;
|
|
return;
|
|
}
|
|
|
|
let self_pos = self.pos.load();
|
|
let holder_pos = holder_entity.pos.load();
|
|
let diff = self_pos - holder_pos;
|
|
let distance = diff.length();
|
|
|
|
if distance > Self::LEASH_SNAP_DISTANCE {
|
|
// Too far: snap/break leash and drop lead item
|
|
self.unleash().await;
|
|
let lead_item =
|
|
pumpkin_data::item_stack::ItemStack::new(1, &pumpkin_data::item::Item::LEAD);
|
|
self.world
|
|
.load()
|
|
.drop_stack(&self.block_pos.load(), lead_item)
|
|
.await;
|
|
} else if distance > Self::LEASH_ELASTIC_DISTANCE {
|
|
// Elastic pull force towards leash holder
|
|
let dir = (holder_pos - self_pos).normalize();
|
|
let pull_strength = (distance - Self::LEASH_ELASTIC_DISTANCE) * 0.11;
|
|
let current_vel = self.velocity.load();
|
|
self.velocity.store(current_vel + dir * pull_strength);
|
|
self.velocity_dirty.store(true, Relaxed);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn has_passengers(&self) -> bool {
|
|
!self.passengers.lock().await.is_empty()
|
|
}
|
|
|
|
pub async fn has_vehicle(&self) -> bool {
|
|
let vehicle = self.vehicle.lock().await;
|
|
vehicle.is_some()
|
|
}
|
|
|
|
pub async fn add_passenger(
|
|
&self,
|
|
vehicle: Arc<dyn EntityBase>,
|
|
passenger: Arc<dyn EntityBase>,
|
|
) {
|
|
let passenger_entity = passenger.get_entity();
|
|
*passenger_entity.vehicle.lock().await = Some(vehicle);
|
|
|
|
let mut passengers = self.passengers.lock().await;
|
|
passengers.push(passenger);
|
|
|
|
let passenger_ids: Vec<VarInt> = passengers
|
|
.iter()
|
|
.map(|p| VarInt(p.get_entity().entity_id))
|
|
.collect();
|
|
|
|
let world = self.world.load();
|
|
let chunk_pos = self.chunk_pos.load();
|
|
world.broadcast_to_chunk(
|
|
chunk_pos,
|
|
&CSetPassengers::new(VarInt(self.entity_id), &passenger_ids),
|
|
);
|
|
}
|
|
|
|
pub(crate) async fn remove_passenger_on_disconnect(&self, passenger_id: i32) {
|
|
let mut passengers = self.passengers.lock().await;
|
|
if let Some(index) = passengers
|
|
.iter()
|
|
.position(|passenger| passenger.get_entity().entity_id == passenger_id)
|
|
{
|
|
let passenger = passengers.remove(index);
|
|
*passenger.get_entity().vehicle.lock().await = None;
|
|
}
|
|
|
|
let passenger_ids: Vec<VarInt> = passengers
|
|
.iter()
|
|
.map(|passenger| VarInt(passenger.get_entity().entity_id))
|
|
.collect();
|
|
drop(passengers);
|
|
|
|
self.world.load().broadcast_to_chunk(
|
|
self.chunk_pos.load(),
|
|
&CSetPassengers::new(VarInt(self.entity_id), &passenger_ids),
|
|
);
|
|
}
|
|
|
|
#[allow(clippy::too_many_lines)]
|
|
pub async fn remove_passenger(&self, passenger_id: i32) {
|
|
let mut passengers = self.passengers.lock().await;
|
|
let removed_passenger = if let Some(idx) = passengers
|
|
.iter()
|
|
.position(|p| p.get_entity().entity_id == passenger_id)
|
|
{
|
|
let passenger = passengers.remove(idx);
|
|
*passenger.get_entity().vehicle.lock().await = None;
|
|
Some(passenger)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let passenger_ids: Vec<VarInt> = passengers
|
|
.iter()
|
|
.map(|p| VarInt(p.get_entity().entity_id))
|
|
.collect();
|
|
drop(passengers);
|
|
|
|
let chunk_pos = self.chunk_pos.load();
|
|
|
|
if let Some(passenger) = removed_passenger {
|
|
let vehicle_box = self.bounding_box.load();
|
|
let passenger_entity = passenger.get_entity();
|
|
|
|
// Pre-allocate teleport ID and block movement packets BEFORE sending
|
|
// CSetPassengers. This prevents a race condition where the client receives
|
|
// the dismount packet, sends stale position packets from the old riding
|
|
// position, and the server processes them before the teleport arrives.
|
|
let teleport_id = if let Some(player) = passenger.get_player() {
|
|
let id = player
|
|
.teleport_id_count
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
|
+ 1;
|
|
// Use fallback position as placeholder — updated below with real position
|
|
let placeholder =
|
|
Vector3::new(self.pos.load().x, vehicle_box.max.y, self.pos.load().z);
|
|
*player.awaiting_teleport.lock().await = Some((id.into(), placeholder));
|
|
Some(id)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Vanilla: ridingCooldown = 60 (prevents immediate re-mount)
|
|
passenger_entity.riding_cooldown.store(60, Relaxed);
|
|
// TODO: world.emitGameEvent(passenger, GameEvent.ENTITY_DISMOUNT, vehicle.pos)
|
|
|
|
// Now send CSetPassengers — client movement is already blocked.
|
|
// Vanilla sends this directly to the dismounting player's connection,
|
|
// then broadcasts to other players separately.
|
|
let world = self.world.load();
|
|
let passengers_packet = CSetPassengers::new(VarInt(self.entity_id), &passenger_ids);
|
|
if let Some(player) = passenger.get_player() {
|
|
player.client.enqueue_packet(&passengers_packet).await;
|
|
world.broadcast_to_chunk_except(
|
|
chunk_pos,
|
|
&[player.get_entity().entity_uuid],
|
|
&passengers_packet,
|
|
);
|
|
} else {
|
|
world.broadcast_to_chunk(chunk_pos, &passengers_packet);
|
|
}
|
|
|
|
// Calculate dismount directions and offsets (vanilla DismountHelper)
|
|
let vehicle_yaw = self.yaw.load();
|
|
// Wrap yaw to 0..360 range
|
|
let wrapped_yaw = (vehicle_yaw % 360.0 + 360.0) % 360.0;
|
|
let forward_dir = if !(45.0..315.0).contains(&wrapped_yaw) {
|
|
BlockDirection::South
|
|
} else if (45.0..135.0).contains(&wrapped_yaw) {
|
|
BlockDirection::West
|
|
} else if (135.0..225.0).contains(&wrapped_yaw) {
|
|
BlockDirection::North
|
|
} else {
|
|
BlockDirection::East
|
|
};
|
|
|
|
let get_step = |dir: BlockDirection| -> (i32, i32) {
|
|
match dir {
|
|
BlockDirection::North => (0, -1),
|
|
BlockDirection::South => (0, 1),
|
|
BlockDirection::East => (1, 0),
|
|
BlockDirection::West => (-1, 0),
|
|
_ => (0, 0),
|
|
}
|
|
};
|
|
|
|
let get_clockwise = |dir: BlockDirection| -> BlockDirection {
|
|
match dir {
|
|
BlockDirection::North => BlockDirection::East,
|
|
BlockDirection::East => BlockDirection::South,
|
|
BlockDirection::South => BlockDirection::West,
|
|
BlockDirection::West => BlockDirection::North,
|
|
other => other,
|
|
}
|
|
};
|
|
|
|
let get_opposite = |dir: BlockDirection| -> BlockDirection {
|
|
match dir {
|
|
BlockDirection::North => BlockDirection::South,
|
|
BlockDirection::South => BlockDirection::North,
|
|
BlockDirection::East => BlockDirection::West,
|
|
BlockDirection::West => BlockDirection::East,
|
|
other => other,
|
|
}
|
|
};
|
|
|
|
let right_dir = get_clockwise(forward_dir);
|
|
let left_dir = get_opposite(right_dir);
|
|
let back_dir = get_opposite(forward_dir);
|
|
|
|
let (fx, fz) = get_step(forward_dir);
|
|
let (rx, rz) = get_step(right_dir);
|
|
let (lx, lz) = get_step(left_dir);
|
|
let (bx, bz) = get_step(back_dir);
|
|
|
|
let offsets = [
|
|
(rx, rz),
|
|
(lx, lz),
|
|
(bx + rx, bz + rz),
|
|
(bx + lx, bz + lz),
|
|
(fx + rx, fz + rz),
|
|
(fx + lx, fz + lz),
|
|
(bx, bz),
|
|
(fx, fz),
|
|
];
|
|
|
|
let target_block_y = vehicle_box.max.y.floor() as i32;
|
|
let below_pos = BlockPos(Vector3::new(
|
|
self.pos.load().x.floor() as i32,
|
|
target_block_y - 1,
|
|
self.pos.load().z.floor() as i32,
|
|
));
|
|
|
|
let below_state_id = world.get_block_state_id(&below_pos);
|
|
// Vanilla: isWater checks specifically for water fluid, not any fluid
|
|
let is_water = Fluid::from_state_id(below_state_id)
|
|
.is_some_and(|f| f.id == Fluid::WATER.id || f.id == Fluid::FLOWING_WATER.id);
|
|
|
|
let fallback_pos =
|
|
Vector3::new(self.pos.load().x, vehicle_box.max.y, self.pos.load().z);
|
|
|
|
let dismount_pos = if is_water {
|
|
fallback_pos
|
|
} else {
|
|
// Vanilla checks Standing, Crouching, Swimming poses and their respective height checks
|
|
let poses_and_heights = [
|
|
(EntityPose::Standing, vec![0, 1, -1]),
|
|
(EntityPose::Crouching, vec![0, 1, -1]),
|
|
(EntityPose::Swimming, vec![0, 1]),
|
|
];
|
|
|
|
let vehicle_block_pos = self.block_pos.load();
|
|
let mut found = None;
|
|
|
|
'search: for (pose, y_offsets) in poses_and_heights {
|
|
let dims = Self::get_entity_dimensions(pose);
|
|
|
|
for y_offset in y_offsets {
|
|
for &(ox, oz) in &offsets {
|
|
let target_block_x = vehicle_block_pos.0.x + ox;
|
|
let target_block_y = vehicle_block_pos.0.y + y_offset;
|
|
let target_block_z = vehicle_block_pos.0.z + oz;
|
|
|
|
let target_pos = BlockPos(Vector3::new(
|
|
target_block_x,
|
|
target_block_y,
|
|
target_block_z,
|
|
));
|
|
let height = world.get_dismount_height(&target_pos);
|
|
|
|
if height.is_finite() && height < 1.0 {
|
|
let location = Vector3::new(
|
|
f64::from(target_block_x) + 0.5,
|
|
f64::from(target_block_y) + height,
|
|
f64::from(target_block_z) + 0.5,
|
|
);
|
|
|
|
let bbox = BoundingBox::new_from_pos(
|
|
location.x, location.y, location.z, &dims,
|
|
);
|
|
if world.is_space_empty(bbox) {
|
|
found = Some((location, pose));
|
|
break 'search;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some((pos, pose)) = found {
|
|
if pose != EntityPose::Standing {
|
|
passenger_entity.set_pose(pose);
|
|
}
|
|
pos
|
|
} else {
|
|
// Try dismounting directly on top of the vehicle as fallback
|
|
let mut found_fallback = None;
|
|
let vehicle_top = vehicle_box.max.y;
|
|
|
|
let poses = [
|
|
EntityPose::Standing,
|
|
EntityPose::Crouching,
|
|
EntityPose::Swimming,
|
|
];
|
|
|
|
for pose in poses {
|
|
let dims = Self::get_entity_dimensions(pose);
|
|
let bbox = BoundingBox::new_from_pos(
|
|
self.pos.load().x,
|
|
vehicle_top,
|
|
self.pos.load().z,
|
|
&dims,
|
|
);
|
|
if world.is_space_empty(bbox) {
|
|
found_fallback = Some((
|
|
Vector3::new(self.pos.load().x, vehicle_top, self.pos.load().z),
|
|
pose,
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some((pos, pose)) = found_fallback {
|
|
if pose != EntityPose::Standing {
|
|
passenger_entity.set_pose(pose);
|
|
}
|
|
pos
|
|
} else {
|
|
fallback_pos
|
|
}
|
|
}
|
|
};
|
|
|
|
if let Some(player) = passenger.get_player() {
|
|
if let Some(id) = teleport_id {
|
|
player.get_entity().set_pos(dismount_pos);
|
|
// Update awaiting_teleport with the real dismount position
|
|
*player.awaiting_teleport.lock().await = Some((id.into(), dismount_pos));
|
|
// Use enqueue_packet (not send_packet_now) so the teleport goes through
|
|
// the same packet queue as CSetPassengers, preserving send order.
|
|
// Vanilla uses DELTA | ROT flags: position absolute, delta/rotation relative.
|
|
// With rotation relative and yaw/pitch=0, the client preserves its current look.
|
|
player
|
|
.client
|
|
.enqueue_packet(&CPlayerPosition::new(
|
|
id.into(),
|
|
dismount_pos,
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
0.0,
|
|
0.0,
|
|
vec![
|
|
PositionFlag::DeltaX,
|
|
PositionFlag::DeltaY,
|
|
PositionFlag::DeltaZ,
|
|
PositionFlag::YRot,
|
|
PositionFlag::XRot,
|
|
],
|
|
))
|
|
.await;
|
|
}
|
|
|
|
// Vanilla: setSneaking(false) after dismount via sneak input
|
|
if passenger_entity.sneaking.load(Relaxed) {
|
|
passenger_entity.set_sneaking(false).await;
|
|
}
|
|
} else {
|
|
passenger_entity.set_pos(dismount_pos);
|
|
}
|
|
} else {
|
|
// No passenger was removed, still need to broadcast the passenger list
|
|
let world = self.world.load();
|
|
world.broadcast_to_chunk(
|
|
chunk_pos,
|
|
&CSetPassengers::new(VarInt(self.entity_id), &passenger_ids),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub async fn check_out_of_world(&self, dyn_self: &dyn EntityBase) {
|
|
if self.pos.load().y < f64::from(self.world.load().dimension.min_y) - 64.0 {
|
|
dyn_self.tick_in_void(dyn_self).await;
|
|
}
|
|
}
|
|
|
|
pub async fn reset_state(&self) {
|
|
self.pose.store(EntityPose::Standing);
|
|
self.fall_flying.store(false, Relaxed);
|
|
self.extinguish();
|
|
self.set_on_fire(false).await;
|
|
}
|
|
|
|
pub async fn slow_movement(&self, state: &BlockState, multiplier: Vector3<f64>) {
|
|
match self.entity_type.id {
|
|
v if v == EntityType::PLAYER.id => {
|
|
if let Some(player_entity) = self.get_player()
|
|
&& player_entity.is_flying().await
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
v if (v == EntityType::SPIDER.id || v == EntityType::CAVE_SPIDER.id)
|
|
&& Block::from_state_id(state.id).id == Block::COBWEB.id =>
|
|
{
|
|
return;
|
|
}
|
|
v if v == EntityType::WITHER.id => {
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(living) = self.get_living_entity() {
|
|
living.fall_distance.store(0f32);
|
|
}
|
|
self.movement_multiplier.store(multiplier);
|
|
}
|
|
}
|
|
|
|
impl NBTStorage for Entity {
|
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
let position = self.pos.load();
|
|
nbt.put_string(
|
|
"id",
|
|
format!("minecraft:{}", self.entity_type.resource_name),
|
|
);
|
|
nbt.put_uuid("UUID", self.entity_uuid);
|
|
nbt.put(
|
|
"Pos",
|
|
NbtTag::List(vec![
|
|
position.x.into(),
|
|
position.y.into(),
|
|
position.z.into(),
|
|
]),
|
|
);
|
|
let velocity = self.velocity.load();
|
|
nbt.put(
|
|
"Motion",
|
|
NbtTag::List(vec![
|
|
velocity.x.into(),
|
|
velocity.y.into(),
|
|
velocity.z.into(),
|
|
]),
|
|
);
|
|
nbt.put(
|
|
"Rotation",
|
|
NbtTag::List(vec![self.yaw.load().into(), self.pitch.load().into()]),
|
|
);
|
|
nbt.put_short("Fire", self.fire_ticks.load(Relaxed) as i16);
|
|
nbt.put_bool("OnGround", self.on_ground.load(Relaxed));
|
|
nbt.put_bool("Invulnerable", self.invulnerable.load(Relaxed));
|
|
nbt.put_int("PortalCooldown", self.portal_cooldown.load(Relaxed) as i32);
|
|
if self.has_visual_fire.load(Relaxed) {
|
|
nbt.put_bool("HasVisualFire", true);
|
|
}
|
|
nbt.put_int("TicksFrozen", self.frozen_ticks.load(Relaxed));
|
|
if let Some(custom_name) = &**self.custom_name.load()
|
|
&& let Ok(name_json) = pumpkin_util::serde_json::to_string(custom_name)
|
|
{
|
|
nbt.put_string("CustomName", name_json);
|
|
}
|
|
nbt.put_bool("CustomNameVisible", self.custom_name_visible.load(Relaxed));
|
|
|
|
let tags = self.scoreboard_tags.lock().await;
|
|
if !tags.is_empty() {
|
|
nbt.put(
|
|
"Tags",
|
|
NbtTag::List(
|
|
tags.iter()
|
|
.map(|tag| NbtTag::String(tag.as_str().into()))
|
|
.collect(),
|
|
),
|
|
);
|
|
}
|
|
|
|
// todo more...
|
|
})
|
|
}
|
|
|
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async {
|
|
if let Some(position) = nbt.get_list("Pos")
|
|
&& position.len() >= 3
|
|
{
|
|
let x = position[0].extract_double().unwrap_or(0.0);
|
|
let y = position[1].extract_double().unwrap_or(0.0);
|
|
let z = position[2].extract_double().unwrap_or(0.0);
|
|
let pos = Vector3::new(x, y, z);
|
|
self.set_pos(pos);
|
|
self.last_sent_pos.store(pos);
|
|
}
|
|
if let Some(velocity) = nbt.get_list("Motion")
|
|
&& velocity.len() >= 3
|
|
{
|
|
let x = velocity[0].extract_double().unwrap_or(0.0);
|
|
let y = velocity[1].extract_double().unwrap_or(0.0);
|
|
let z = velocity[2].extract_double().unwrap_or(0.0);
|
|
self.velocity.store(Vector3::new(x, y, z));
|
|
}
|
|
if let Some(rotation) = nbt.get_list("Rotation")
|
|
&& rotation.len() >= 2
|
|
{
|
|
let yaw = rotation[0].extract_float().unwrap_or(0.0);
|
|
let pitch = rotation[1].extract_float().unwrap_or(0.0);
|
|
self.set_rotation(yaw, pitch);
|
|
let yaw_byte = (yaw * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
|
let pitch_byte = (pitch * 256.0 / 360.0).rem_euclid(256.0) as u8;
|
|
self.last_sent_yaw.store(yaw_byte, Relaxed);
|
|
self.last_sent_pitch.store(pitch_byte, Relaxed);
|
|
self.head_yaw.store(yaw);
|
|
self.last_sent_head_yaw.store(yaw_byte, Relaxed);
|
|
}
|
|
self.fire_ticks
|
|
.store(i32::from(nbt.get_short("Fire").unwrap_or(0)), Relaxed);
|
|
self.on_ground
|
|
.store(nbt.get_bool("OnGround").unwrap_or(false), Relaxed);
|
|
self.invulnerable
|
|
.store(nbt.get_bool("Invulnerable").unwrap_or(false), Relaxed);
|
|
self.portal_cooldown
|
|
.store(nbt.get_int("PortalCooldown").unwrap_or(0) as u32, Relaxed);
|
|
self.has_visual_fire
|
|
.store(nbt.get_bool("HasVisualFire").unwrap_or(false), Relaxed);
|
|
self.frozen_ticks
|
|
.store(nbt.get_int("TicksFrozen").unwrap_or(0), Relaxed);
|
|
if let Some(name_json) = nbt.get_string("CustomName")
|
|
&& let Ok(component) = pumpkin_util::serde_json::from_str(name_json)
|
|
{
|
|
self.custom_name.store(Arc::new(Some(component)));
|
|
}
|
|
self.custom_name_visible
|
|
.store(nbt.get_bool("CustomNameVisible").unwrap_or(false), Relaxed);
|
|
|
|
if let Some(tag_list) = nbt.get_list("Tags") {
|
|
let mut tags = self.scoreboard_tags.lock().await;
|
|
tags.clear();
|
|
tags.extend(
|
|
tag_list
|
|
.iter()
|
|
.filter_map(|tag| tag.extract_string().map(str::to_owned))
|
|
.take(MAX_SCOREBOARD_TAGS),
|
|
);
|
|
}
|
|
|
|
// todo more...
|
|
})
|
|
}
|
|
}
|
|
|
|
impl EntityBase for Entity {
|
|
fn tick<'a>(
|
|
&'a self,
|
|
caller: &'a Arc<dyn EntityBase>,
|
|
_server: &'a Server,
|
|
) -> EntityBaseFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
// Recomputed during movement/block-collision handling in the same tick.
|
|
let was_in_powder_snow = self.is_in_powder_snow.load(Ordering::Relaxed);
|
|
self.was_in_powder_snow
|
|
.store(was_in_powder_snow, Ordering::Relaxed);
|
|
self.is_in_powder_snow.store(false, Ordering::Relaxed);
|
|
|
|
let block_pos = self.block_pos.load();
|
|
if self.last_biome_update_pos.load() != block_pos {
|
|
let world = self.world.load();
|
|
let biome = world.level.get_rough_biome(&block_pos);
|
|
self.current_biome.store(Arc::new(biome));
|
|
self.last_biome_update_pos.store(block_pos);
|
|
}
|
|
|
|
self.update_last_pos();
|
|
self.tick_portal(caller).await;
|
|
self.update_fluid_state(caller).await;
|
|
self.check_out_of_world(&**caller).await;
|
|
let fire_ticks = self.fire_ticks.load(Ordering::Relaxed);
|
|
|
|
// Check for fire immunity (or if the specific entity is)
|
|
let is_immune =
|
|
self.entity_type.fire_immune || self.fire_immune.load(Ordering::Relaxed);
|
|
if fire_ticks > 0 {
|
|
if is_immune {
|
|
self.fire_ticks.store(fire_ticks - 4, Ordering::Relaxed);
|
|
if self.fire_ticks.load(Ordering::Relaxed) < 0 {
|
|
self.extinguish();
|
|
}
|
|
} else {
|
|
if fire_ticks % 20 == 0 {
|
|
(**caller).damage(&**caller, 1.0, DamageType::ON_FIRE).await;
|
|
}
|
|
|
|
self.fire_ticks.store(fire_ticks - 1, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
// Check if visual fire should be sent
|
|
let should_render_fire = self.fire_ticks.load(Ordering::Relaxed) > 0 && !is_immune;
|
|
self.set_on_fire(should_render_fire).await;
|
|
|
|
let riding_cooldown = self.riding_cooldown.load(Ordering::Relaxed);
|
|
if riding_cooldown > 0 {
|
|
self.riding_cooldown
|
|
.store(riding_cooldown - 1, Ordering::Relaxed);
|
|
}
|
|
})
|
|
}
|
|
|
|
fn teleport(
|
|
self: Arc<Self>,
|
|
position: Vector3<f64>,
|
|
yaw: Option<f32>,
|
|
pitch: Option<f32>,
|
|
world: Arc<World>,
|
|
) -> TeleportFuture {
|
|
// TODO: handle world change
|
|
Box::pin(async move {
|
|
self.get_entity().teleport(position, yaw, pitch, world);
|
|
})
|
|
}
|
|
|
|
fn get_entity(&self) -> &Entity {
|
|
self
|
|
}
|
|
|
|
fn get_living_entity(&self) -> Option<&LivingEntity> {
|
|
None
|
|
}
|
|
|
|
fn cast_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn as_nbt_storage(&self) -> &dyn NBTStorage {
|
|
self
|
|
}
|
|
}
|
|
|
|
pub type NbtFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
|
|
|
pub trait NBTStorage: Send + Sync {
|
|
fn write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async {})
|
|
}
|
|
|
|
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
self.read_nbt_non_mut(nbt).await;
|
|
})
|
|
}
|
|
|
|
fn read_nbt_non_mut<'a>(&'a self, _nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
|
Box::pin(async {})
|
|
}
|
|
}
|
|
|
|
pub type NBTInitFuture<'a, T> = Pin<Box<dyn Future<Output = Option<T>> + Send + 'a>>;
|
|
|
|
pub trait NBTStorageInit: Send + Sync + Sized {
|
|
fn create_from_nbt<'a>(_nbt: &'a mut NbtCompound) -> NBTInitFuture<'a, Self>
|
|
where
|
|
Self: 'a,
|
|
{
|
|
Box::pin(async move { None })
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
/// Represents various entity flags that are sent in entity metadata.
|
|
///
|
|
/// These flags are used by the client to modify the rendering of entities based on their current state.
|
|
///
|
|
/// **Purpose:**
|
|
///
|
|
/// This enum provides a more type-safe and readable way to represent entity flags compared to using raw integer values.
|
|
pub enum Flag {
|
|
/// Indicates if the entity is on fire.
|
|
OnFire = 0,
|
|
/// Indicates if the entity is sneaking.
|
|
Sneaking = 1,
|
|
/// Indicates if the entity is sprinting.
|
|
Sprinting = 3,
|
|
/// Indicates if the entity is swimming.
|
|
Swimming = 4,
|
|
/// Indicates if the entity is invisible.
|
|
Invisible = 5,
|
|
/// Indicates if the entity is glowing.
|
|
Glowing = 6,
|
|
/// Indicates if the entity is flying due to a fall.
|
|
FallFlying = 7,
|
|
}
|
|
|
|
impl Flag {
|
|
#[must_use]
|
|
pub const fn to_bedrock(&self) -> Option<u32> {
|
|
match self {
|
|
Self::OnFire => Some(entity_data_flag::ON_FIRE),
|
|
Self::Sneaking => Some(entity_data_flag::SNEAKING),
|
|
Self::Sprinting => Some(entity_data_flag::SPRINTING),
|
|
Self::Swimming => Some(entity_data_flag::SWIMMING),
|
|
Self::Invisible => Some(entity_data_flag::INVISIBLE),
|
|
Self::FallFlying => Some(entity_data_flag::GLIDING),
|
|
Self::Glowing => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn equipment_break_status_maps_all_slots() {
|
|
// Status bytes from vanilla EntityEvent: mainhand=47, offhand=48,
|
|
// head=49, chest=50, legs=51, feet=52, body=65, saddle=68.
|
|
let cases: &[(&EquipmentSlot, u8)] = &[
|
|
(&EquipmentSlot::MAIN_HAND, EntityStatus::MainhandBreak as u8),
|
|
(&EquipmentSlot::OFF_HAND, EntityStatus::OffhandBreak as u8),
|
|
(&EquipmentSlot::HEAD, EntityStatus::HeadBreak as u8),
|
|
(&EquipmentSlot::CHEST, EntityStatus::ChestBreak as u8),
|
|
(&EquipmentSlot::LEGS, EntityStatus::LegsBreak as u8),
|
|
(&EquipmentSlot::FEET, EntityStatus::FeetBreak as u8),
|
|
(&EquipmentSlot::BODY, EntityStatus::BodyBreak as u8),
|
|
(&EquipmentSlot::SADDLE, EntityStatus::SaddleBreak as u8),
|
|
];
|
|
for (i, (slot, expected)) in cases.iter().enumerate() {
|
|
assert_eq!(
|
|
equipment_break_status(slot) as u8,
|
|
*expected,
|
|
"status mismatch at index {i}"
|
|
);
|
|
}
|
|
}
|
|
}
|