mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-10-31 04:17:44 +00:00
fix(item-stacking-damage): vanilla dropped items + tag-based damage (#3711)
* item stacking inital commit * typo fix * typo comment entitie * own lava damage * one fluid interaction update
This commit is contained in:
@@ -119,8 +119,7 @@ impl FireBlockBase {
|
||||
/// Shared fire collision behavior used by `fire` and `soul_fire`.
|
||||
pub fn apply_fire_collision(args: &OnEntityCollisionArgs<'_>, extra_damage_for_living: bool) {
|
||||
let base_entity = args.entity.get_entity();
|
||||
if !base_entity.entity_type.fire_immune && !base_entity.fire_immune.load(Ordering::Relaxed)
|
||||
{
|
||||
if !base_entity.is_fire_immune() {
|
||||
let ticks = base_entity.fire_ticks.load(Ordering::Relaxed);
|
||||
|
||||
// Timer logic
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use pumpkin_data::{
|
||||
Enchantment, damage::DamageType, data_component_impl::EquipmentSlot, effect::StatusEffect,
|
||||
};
|
||||
@@ -26,7 +24,7 @@ impl BlockBehaviour for MagmaBlock {
|
||||
}
|
||||
|
||||
// Fire immune entities don't take damage
|
||||
if ent.entity_type.fire_immune || ent.fire_immune.load(Ordering::Relaxed) {
|
||||
if ent.is_fire_immune() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::{tick::TickPriority, world::BlockFlags};
|
||||
use std::sync::Arc;
|
||||
type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub struct FlowingLava;
|
||||
|
||||
@@ -181,8 +180,7 @@ impl FluidBehaviour for FlowingLava {
|
||||
|
||||
fn on_entity_collision(&self, entity: &dyn EntityBase) {
|
||||
let base_entity = entity.get_entity();
|
||||
if !base_entity.entity_type.fire_immune && !base_entity.fire_immune.load(Ordering::Relaxed)
|
||||
{
|
||||
if !base_entity.is_fire_immune() {
|
||||
entity.set_on_fire_for(15.0);
|
||||
|
||||
// Also apply lava damage
|
||||
|
||||
@@ -10,6 +10,7 @@ use pumpkin_data::{
|
||||
item::Item,
|
||||
particle::Particle,
|
||||
sound::{Sound, SoundCategory},
|
||||
tag::{self, Taggable},
|
||||
};
|
||||
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
|
||||
use pumpkin_util::math::{euler_angle::EulerAngle, vector3::Vector3};
|
||||
@@ -328,28 +329,22 @@ impl EntityBase for ArmorStandEntity {
|
||||
game_rules.mob_griefing
|
||||
};
|
||||
|
||||
if !mob_griefing_gamerule && source.is_some_and(|source| source.get_player().is_none()) {
|
||||
// Vanilla: only mob attackers are stopped by `mobGriefing`.
|
||||
if !mob_griefing_gamerule && cause.is_some_and(|cause| cause.get_mob().is_some()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let bypasses_invulnerability =
|
||||
damage_type == DamageType::OUT_OF_WORLD || damage_type == DamageType::GENERIC_KILL;
|
||||
|
||||
if bypasses_invulnerability {
|
||||
if damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_INVULNERABILITY) {
|
||||
entity.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
if entity.is_invulnerable_to(&damage_type) || self.is_invisible() || self.is_marker() {
|
||||
if entity.is_invulnerable_to(&damage_type, cause) || self.is_invisible() || self.is_marker()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_explosion = damage_type == DamageType::FIREWORKS
|
||||
|| damage_type == DamageType::EXPLOSION
|
||||
|| damage_type == DamageType::PLAYER_EXPLOSION
|
||||
|| damage_type == DamageType::BAD_RESPAWN_POINT;
|
||||
|
||||
if is_explosion {
|
||||
if damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_EXPLOSION) {
|
||||
Self::on_break(entity);
|
||||
entity.remove();
|
||||
return false;
|
||||
@@ -358,16 +353,9 @@ impl EntityBase for ArmorStandEntity {
|
||||
// TODO: IGNITES_ARMOR_STANDS (in_fire, campfire) - set on fire
|
||||
// TODO: BURNS_ARMOR_STANDS (on_fire) - reduce health
|
||||
|
||||
let can_break = damage_type == DamageType::PLAYER_EXPLOSION
|
||||
|| damage_type == DamageType::PLAYER_ATTACK
|
||||
|| damage_type == DamageType::SPEAR
|
||||
|| damage_type == DamageType::MACE_SMASH;
|
||||
|
||||
let always_kills = damage_type == DamageType::ARROW
|
||||
|| damage_type == DamageType::TRIDENT
|
||||
|| damage_type == DamageType::FIREBALL
|
||||
|| damage_type == DamageType::WITHER_SKULL
|
||||
|| damage_type == DamageType::WIND_CHARGE;
|
||||
let can_break = damage_type.has_tag(&tag::DamageType::MINECRAFT_CAN_BREAK_ARMOR_STAND);
|
||||
let always_kills =
|
||||
damage_type.has_tag(&tag::DamageType::MINECRAFT_ALWAYS_KILLS_ARMOR_STANDS);
|
||||
|
||||
if !can_break && !always_kills {
|
||||
return false;
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
use crate::entity::player::statistics::StatisticCategory;
|
||||
use crate::server::Server;
|
||||
use crate::world::World;
|
||||
use core::f32;
|
||||
use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::data_component_impl::DamageResistantImpl;
|
||||
use pumpkin_data::data_component_impl::DamageResistantType;
|
||||
use pumpkin_data::data_component_impl::{BundleContentsImpl, ContainerImpl, DamageResistantImpl};
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::game_event::GameEvent;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::tag::Taggable;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_protocol::bedrock::client::CAddItemActor;
|
||||
use pumpkin_protocol::bedrock::network_item::ItemStackWrapper;
|
||||
use pumpkin_protocol::bedrock::server::actor_event::{ActorEventID, SActorEvent};
|
||||
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
|
||||
use pumpkin_protocol::codec::var_int::VarInt;
|
||||
use pumpkin_protocol::codec::var_long::VarLong;
|
||||
use pumpkin_protocol::codec::var_ulong::VarULong;
|
||||
use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata};
|
||||
use pumpkin_util::math::atomic_f32::AtomicF32;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use std::sync::atomic::Ordering::{AcqRel, Relaxed};
|
||||
|
||||
@@ -78,6 +84,11 @@ impl Drop for ItemMergeReservation<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vanilla `ItemEntity.LIFETIME`.
|
||||
const LIFETIME: u32 = 6000; // 5 minutes in ticks
|
||||
/// Vanilla `ItemEntity.merge(to, from, 64)`: cap on top of `getMaxStackSize`.
|
||||
const MERGE_MAX_COUNT: u8 = 64;
|
||||
|
||||
impl ItemEntity {
|
||||
pub const DEFAULT_PICKUP_DELAY: u8 = 10;
|
||||
|
||||
@@ -88,13 +99,7 @@ impl ItemEntity {
|
||||
rand::random::<f64>().mul_add(0.2, -0.1),
|
||||
));
|
||||
entity.yaw.store(rand::random::<f32>() * 360.0);
|
||||
|
||||
// Set fire immunity for certain items
|
||||
if let Some(res) = item_stack.get_data_component::<DamageResistantImpl>()
|
||||
&& res.res_type == DamageResistantType::Fire
|
||||
{
|
||||
entity.fire_immune.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Self::update_fire_immune(&entity, &item_stack);
|
||||
|
||||
Self {
|
||||
entity,
|
||||
@@ -116,13 +121,7 @@ impl ItemEntity {
|
||||
) -> Self {
|
||||
entity.velocity.store(velocity);
|
||||
entity.yaw.store(rand::random::<f32>() * 360.0);
|
||||
|
||||
// Set fire immunity for certain items
|
||||
if let Some(res) = item_stack.get_data_component::<DamageResistantImpl>()
|
||||
&& res.res_type == DamageResistantType::Fire
|
||||
{
|
||||
entity.fire_immune.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Self::update_fire_immune(&entity, &item_stack);
|
||||
|
||||
Self {
|
||||
entity,
|
||||
@@ -167,7 +166,38 @@ impl ItemEntity {
|
||||
&self.entity
|
||||
}
|
||||
|
||||
pub fn can_merge(&self) -> bool {
|
||||
/// Vanilla `ItemStack.canBeHurtBy`: the `damage_resistant` tag blocks matching damage.
|
||||
fn can_be_hurt_by(stack: &ItemStack, damage_type: DamageType) -> bool {
|
||||
stack
|
||||
.get_data_component::<DamageResistantImpl>()
|
||||
.is_none_or(|res| damage_type.is_tagged_with(res.res_type.as_str()) != Some(true))
|
||||
}
|
||||
|
||||
/// Vanilla `ItemEntity.fireImmune`, cached since the item only changes on load.
|
||||
fn update_fire_immune(entity: &Entity, stack: &ItemStack) {
|
||||
entity.fire_immune.store(
|
||||
!Self::can_be_hurt_by(stack, DamageType::IN_FIRE),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Vanilla `ItemEntity.areMergable`.
|
||||
#[must_use]
|
||||
pub fn are_mergeable(stack: &ItemStack, other: &ItemStack) -> bool {
|
||||
u16::from(stack.item_count) + u16::from(other.item_count)
|
||||
<= u16::from(other.get_max_stack_size())
|
||||
&& stack.are_items_and_components_equal(other)
|
||||
}
|
||||
|
||||
/// Vanilla `ItemEntity.isMergable`.
|
||||
fn is_mergeable(&self) -> bool {
|
||||
if self.entity.removed.load(Ordering::SeqCst)
|
||||
|| self.never_pickup.load(Ordering::Relaxed)
|
||||
|| self.never_despawn.load(Ordering::Relaxed)
|
||||
|| self.item_age.load(Ordering::Relaxed) >= LIFETIME
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Ok(item_stack) = self.item_stack.try_lock() else {
|
||||
return false;
|
||||
};
|
||||
@@ -175,37 +205,25 @@ impl ItemEntity {
|
||||
item_stack.item_count < item_stack.get_max_stack_size()
|
||||
}
|
||||
|
||||
pub fn try_merge(&self) {
|
||||
if !self.can_merge() || self.never_despawn.load(Ordering::Relaxed) {
|
||||
/// Vanilla `ItemEntity.mergeWithNeighbours`.
|
||||
fn merge_with_neighbours(&self) {
|
||||
if !self.is_mergeable() {
|
||||
return;
|
||||
}
|
||||
|
||||
let bounding_box = self.entity.bounding_box.load().expand(0.5, 0.0, 0.5);
|
||||
|
||||
let world = self.entity.world.load();
|
||||
let entities = world.entities.load();
|
||||
let items: Vec<&Self> = entities
|
||||
.iter()
|
||||
.filter_map(|entity: &Arc<dyn EntityBase>| {
|
||||
entity.get_item_entity().filter(|item| {
|
||||
item.entity.entity_id != self.entity.entity_id
|
||||
&& !item.never_despawn.load(Ordering::Relaxed)
|
||||
&& item.entity.bounding_box.load().intersects(&bounding_box)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for other in world.get_entities_at_box(&bounding_box) {
|
||||
let Some(item) = other.get_item_entity() else {
|
||||
continue;
|
||||
};
|
||||
if item.entity.entity_id == self.entity.entity_id || !item.is_mergeable() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for item in items {
|
||||
if item.can_merge() {
|
||||
if let Some(this_base) = world.get_entity_by_id(self.entity.entity_id)
|
||||
&& let Some(this_item) = this_base.get_item_entity()
|
||||
{
|
||||
this_item.try_merge_with(item);
|
||||
}
|
||||
|
||||
if self.entity.removed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
self.try_merge_with(item);
|
||||
if self.entity.removed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,9 +256,7 @@ impl ItemEntity {
|
||||
(&*high_stack, &*low_stack)
|
||||
};
|
||||
|
||||
if !self_stack.are_equal(other_stack)
|
||||
|| self_stack.item_count + other_stack.item_count > self_stack.get_max_stack_size()
|
||||
{
|
||||
if !Self::are_mergeable(self_stack, other_stack) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,8 +315,7 @@ impl ItemEntity {
|
||||
} else {
|
||||
(high_stack, low_stack)
|
||||
};
|
||||
if !self_stack.are_equal(&other_stack)
|
||||
|| self_stack.item_count + other_stack.item_count > self_stack.get_max_stack_size()
|
||||
if !Self::are_mergeable(&self_stack, &other_stack)
|
||||
|| (other_stack.item_count < self_stack.item_count) != target_is_self
|
||||
{
|
||||
return;
|
||||
@@ -311,61 +326,89 @@ impl ItemEntity {
|
||||
(other_stack, self_stack)
|
||||
};
|
||||
|
||||
// Vanilla code adds a .min(64). Not needed with Vanilla item data
|
||||
|
||||
let max_size = stack1.get_max_stack_size();
|
||||
|
||||
let j = stack2.item_count.min(max_size - stack1.item_count);
|
||||
|
||||
stack1.increment(j);
|
||||
|
||||
stack2.decrement(j);
|
||||
|
||||
let empty1 = stack1.item_count == 0;
|
||||
|
||||
let empty2 = stack2.item_count == 0;
|
||||
|
||||
// Vanilla `ItemEntity.merge(to, from, 64)`.
|
||||
let max_size = stack1.get_max_stack_size().min(MERGE_MAX_COUNT);
|
||||
let moved = stack2
|
||||
.item_count
|
||||
.min(max_size.saturating_sub(stack1.item_count));
|
||||
stack1.increment(moved);
|
||||
stack2.decrement(moved);
|
||||
let source_empty = stack2.is_empty();
|
||||
drop(stack1);
|
||||
|
||||
drop(stack2);
|
||||
|
||||
let never_despawn = source.never_despawn.load(Ordering::Relaxed);
|
||||
// `is_mergeable` already excluded never-despawn and never-pickup items.
|
||||
target.pickup_delay.fetch_max(
|
||||
source.pickup_delay.load(Ordering::Relaxed),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
target
|
||||
.item_age
|
||||
.fetch_min(source.item_age.load(Ordering::Relaxed), Ordering::Relaxed);
|
||||
|
||||
target.never_despawn.store(never_despawn, Ordering::Relaxed);
|
||||
|
||||
if !never_despawn {
|
||||
let age = target
|
||||
.item_age
|
||||
.load(Ordering::Relaxed)
|
||||
.min(source.item_age.load(Ordering::Relaxed));
|
||||
|
||||
target.item_age.store(age, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
let never_pickup = source.never_pickup.load(Ordering::Relaxed);
|
||||
|
||||
target.never_pickup.store(never_pickup, Ordering::Relaxed);
|
||||
|
||||
if !never_pickup {
|
||||
let source_delay = source.pickup_delay.load(Ordering::Relaxed);
|
||||
target
|
||||
.pickup_delay
|
||||
.fetch_max(source_delay, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if empty1 {
|
||||
target.entity.remove();
|
||||
} else {
|
||||
target.init_data_tracker();
|
||||
}
|
||||
|
||||
if empty2 {
|
||||
target.on_count_changed();
|
||||
if source_empty {
|
||||
source.entity.remove();
|
||||
} else {
|
||||
source.init_data_tracker();
|
||||
source.on_count_changed();
|
||||
}
|
||||
}
|
||||
|
||||
/// Vanilla `Item.onDestroyed`: containers (`BlockItem`, e.g. shulker boxes and bundles
|
||||
/// spill their contents where the item died, whatever the damage type was).
|
||||
fn on_destroyed(&self, world: &Arc<World>) {
|
||||
let contents = {
|
||||
let mut stack = self
|
||||
.item_stack
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut contents = Vec::new();
|
||||
if let Some(container) = stack.get_data_component::<ContainerImpl>()
|
||||
&& !container.items.is_empty()
|
||||
{
|
||||
contents.extend(container.items.iter().map(|(_, item)| item.clone()));
|
||||
stack.set_data_component(ContainerImpl { items: Vec::new() });
|
||||
}
|
||||
if let Some(bundle) = stack.get_data_component::<BundleContentsImpl>()
|
||||
&& !bundle.items.is_empty()
|
||||
{
|
||||
contents.extend(bundle.items.iter().cloned());
|
||||
stack.set_data_component(BundleContentsImpl { items: Vec::new() });
|
||||
}
|
||||
contents
|
||||
};
|
||||
|
||||
// no pickup delay.
|
||||
let pos = self.entity.pos.load();
|
||||
for item in contents.into_iter().filter(|item| !item.is_empty()) {
|
||||
let spilled = Self::new(Entity::new(world.clone(), pos, &EntityType::ITEM), item);
|
||||
spilled.set_pickup_delay(0);
|
||||
world.spawn_entity(Arc::new(spilled));
|
||||
}
|
||||
}
|
||||
|
||||
/// Resends the stack after its count changed. Bedrock item actors ignore the `ITEM`
|
||||
/// metadata, so they get the count as `UpdateStackSize`
|
||||
fn on_count_changed(&self) {
|
||||
self.init_data_tracker();
|
||||
|
||||
let count = self
|
||||
.item_stack
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.item_count;
|
||||
let packet = SActorEvent {
|
||||
target_runtime_id: VarULong(self.entity.entity_id as u64),
|
||||
event_id: ActorEventID::UpdateStackSize,
|
||||
data: VarInt(i32::from(count)),
|
||||
fire_at_position: None,
|
||||
};
|
||||
self.entity
|
||||
.world
|
||||
.load()
|
||||
.send_to_tracking_players_bedrock(&self.entity, &packet);
|
||||
}
|
||||
|
||||
fn decrement_pickup_delay(&self) {
|
||||
self.pickup_delay
|
||||
.try_update(Ordering::Relaxed, Ordering::Relaxed, |val| {
|
||||
@@ -417,22 +460,18 @@ impl ItemEntity {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_tick_move(&self, move_velo: Vector3<f64>) -> Option<bool> {
|
||||
/// a resting item only moves every 4th `tickCount`, offset by its id.
|
||||
fn should_tick_move(&self, move_velo: Vector3<f64>) -> bool {
|
||||
let entity = &self.entity;
|
||||
|
||||
let mut tick_move = !entity.on_ground.load(Ordering::SeqCst)
|
||||
|| move_velo.horizontal_length_squared() > 1.0e-5;
|
||||
|
||||
if !tick_move {
|
||||
let Ok(item_age) = i32::try_from(self.item_age.load(Ordering::Relaxed)) else {
|
||||
entity.remove();
|
||||
return None;
|
||||
};
|
||||
|
||||
tick_move = (item_age + entity.entity_id) % 4 == 0;
|
||||
}
|
||||
|
||||
Some(tick_move)
|
||||
!entity.on_ground.load(Ordering::SeqCst)
|
||||
|| move_velo.horizontal_length_squared() > 1.0e-5
|
||||
|| entity
|
||||
.age
|
||||
.load(Ordering::Relaxed)
|
||||
.wrapping_add(entity.entity_id)
|
||||
% 4
|
||||
== 0
|
||||
}
|
||||
|
||||
fn move_and_apply_friction(&self, caller: &dyn EntityBase, move_velo: Vector3<f64>) {
|
||||
@@ -441,19 +480,22 @@ impl ItemEntity {
|
||||
entity.move_entity(caller, move_velo);
|
||||
entity.tick_block_collisions(caller);
|
||||
|
||||
let mut friction = 0.98;
|
||||
// air drag
|
||||
let air_drag = 0.98;
|
||||
let mut friction = air_drag;
|
||||
let on_ground = entity.on_ground.load(Ordering::SeqCst);
|
||||
|
||||
let mut velo = entity.velocity.load();
|
||||
if on_ground {
|
||||
let block_affecting_velo = entity.get_block_with_y_offset(0.999_999).1;
|
||||
friction *= f64::from(block_affecting_velo.slipperiness) * 0.98;
|
||||
friction *= f64::from(block_affecting_velo.slipperiness);
|
||||
}
|
||||
|
||||
velo = velo.multiply(friction, 0.98, friction);
|
||||
velo = velo.multiply(friction, air_drag, friction);
|
||||
|
||||
// a landing item bounces back up with half its speed
|
||||
if on_ground && velo.y < 0.0 {
|
||||
velo.y = 0.0;
|
||||
velo.y *= -0.5;
|
||||
}
|
||||
|
||||
entity.velocity.store(velo);
|
||||
@@ -467,7 +509,7 @@ impl ItemEntity {
|
||||
let entity = &self.entity;
|
||||
let age = self.item_age.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
if age >= 6000 {
|
||||
if age >= LIFETIME {
|
||||
let entity_id = entity.entity_id;
|
||||
let world = entity.world.load_full();
|
||||
let mut despawn_event =
|
||||
@@ -485,20 +527,12 @@ impl ItemEntity {
|
||||
return false;
|
||||
}
|
||||
|
||||
let n = if entity
|
||||
.last_pos
|
||||
.load()
|
||||
.sub(&entity.pos.load())
|
||||
.length_squared()
|
||||
== 0.0
|
||||
{
|
||||
40
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
if age.is_multiple_of(n) && self.can_merge() {
|
||||
self.try_merge();
|
||||
// merge rate on `tickCount`: 2 while the item changes block cell, else 40.
|
||||
let moved =
|
||||
BlockPos::floored_v(entity.last_pos.load()) != BlockPos::floored_v(entity.pos.load());
|
||||
let rate = if moved { 2 } else { 40 };
|
||||
if entity.age.load(Ordering::Relaxed) % rate == 0 {
|
||||
self.merge_with_neighbours();
|
||||
}
|
||||
|
||||
true
|
||||
@@ -508,20 +542,35 @@ impl ItemEntity {
|
||||
fn mark_needs_sync(&self, caller: &dyn EntityBase, original_velo: Vector3<f64>) {
|
||||
let entity = &self.entity;
|
||||
|
||||
entity.update_fluid_state(caller);
|
||||
// Vanilla runs `updateFluidInteraction` again here, but the fluid damage only
|
||||
// comes from the block effects, so it must not be applied a second time.
|
||||
let in_fluid = !entity.update_fluid_interaction(caller).is_empty();
|
||||
|
||||
if entity.touching_water.load(Ordering::SeqCst)
|
||||
|| entity.touching_lava.load(Ordering::SeqCst)
|
||||
|| entity.velocity.load().sub(&original_velo).length_squared() > 0.01
|
||||
{
|
||||
if in_fluid || entity.velocity.load().sub(&original_velo).length_squared() > 0.01 {
|
||||
entity.velocity_dirty.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityBase for ItemEntity {
|
||||
fn tick(&self, caller: &dyn EntityBase, _server: &Server) {
|
||||
fn tick(&self, caller: &dyn EntityBase, server: &Server) {
|
||||
let entity = &self.entity;
|
||||
if self
|
||||
.item_stack
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.is_empty()
|
||||
{
|
||||
entity.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Vanilla `super.tick()`: `xo/yo/zo`, fluids, fire, portals and the void.
|
||||
entity.tick(caller, server);
|
||||
if entity.removed.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.decrement_pickup_delay();
|
||||
|
||||
let original_velo = entity.velocity.load();
|
||||
@@ -533,11 +582,7 @@ impl EntityBase for ItemEntity {
|
||||
|
||||
let move_velo = entity.velocity.load(); // In case push_out_of_blocks modifies it
|
||||
|
||||
let Some(tick_move) = self.should_tick_move(move_velo) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if tick_move {
|
||||
if self.should_tick_move(move_velo) {
|
||||
self.move_and_apply_friction(caller, move_velo);
|
||||
}
|
||||
|
||||
@@ -565,30 +610,65 @@ impl EntityBase for ItemEntity {
|
||||
damage_type: DamageType,
|
||||
_position: Option<Vector3<f64>>,
|
||||
_source: Option<&dyn EntityBase>,
|
||||
_cause: Option<&dyn EntityBase>,
|
||||
cause: Option<&dyn EntityBase>,
|
||||
) -> bool {
|
||||
// Check if entity is fire_immune
|
||||
let is_fire_damage = damage_type == DamageType::IN_FIRE
|
||||
|| damage_type == DamageType::ON_FIRE
|
||||
|| damage_type == DamageType::LAVA;
|
||||
if is_fire_damage && self.entity.fire_immune.load(Ordering::Relaxed) {
|
||||
// Vanilla `ItemEntity.hurtServer`.
|
||||
let entity = &self.entity;
|
||||
if entity.is_invulnerable_to(&damage_type, cause) {
|
||||
return false;
|
||||
}
|
||||
let world = entity.world.load_full();
|
||||
if !world.level_info.load().game_rules.mob_griefing
|
||||
&& cause.is_some_and(|cause| cause.get_mob().is_some())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// E.g. netherite ignores fire, the nether star explosions.
|
||||
let can_be_hurt = Self::can_be_hurt_by(
|
||||
&self
|
||||
.item_stack
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
damage_type,
|
||||
);
|
||||
if !can_be_hurt {
|
||||
return false;
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut event = crate::plugin::api::events::entity::entity_damage::EntityDamageEvent::new(
|
||||
entity.entity_id,
|
||||
damage_type,
|
||||
amount,
|
||||
);
|
||||
if let Some(server) = world.server.upgrade() {
|
||||
server.plugin_manager.fire_blocking(&server, &mut event);
|
||||
}
|
||||
if event.cancelled {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vanilla `markHurt`: resend the motion.
|
||||
entity.velocity_dirty.store(true, Ordering::SeqCst);
|
||||
|
||||
// Vanilla keeps item health as an int: `(int)(health - damage)`.
|
||||
let destroyed = loop {
|
||||
let current = self.health.load(Relaxed);
|
||||
let new = current - amount;
|
||||
let new = (current - event.damage).trunc();
|
||||
if self
|
||||
.health
|
||||
.compare_exchange(current, new, AcqRel, Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
if new <= 0.0 {
|
||||
self.entity.remove();
|
||||
}
|
||||
return true;
|
||||
break current > 0.0 && new <= 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
world.emit_game_event(GameEvent::EntityDamage.name(), entity.pos.load());
|
||||
if destroyed {
|
||||
self.on_destroyed(&world);
|
||||
entity.remove();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn on_player_collision(&self, player: &Arc<Player>) {
|
||||
@@ -652,7 +732,7 @@ impl EntityBase for ItemEntity {
|
||||
if is_empty {
|
||||
self.entity.remove();
|
||||
} else {
|
||||
self.init_data_tracker();
|
||||
self.on_count_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -699,6 +779,8 @@ impl EntityBase for ItemEntity {
|
||||
if let Some(item_compound) = nbt.get_compound("Item")
|
||||
&& let Some(stack) = ItemStack::read_item_stack(item_compound)
|
||||
{
|
||||
// `new_empty` had no item yet to derive this from.
|
||||
Self::update_fire_immune(&self.entity, &stack);
|
||||
*self
|
||||
.item_stack
|
||||
.lock()
|
||||
@@ -776,3 +858,69 @@ impl EntityBase for ItemEntity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ItemEntity;
|
||||
use pumpkin_data::data_component_impl::{CustomDataImpl, CustomNameImpl};
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::{Enchantment, damage::DamageType};
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::text::TextComponent;
|
||||
|
||||
#[test]
|
||||
fn different_counts_merge_up_to_max_stack() {
|
||||
let stone = |count| ItemStack::new(count, &Item::STONE);
|
||||
assert!(ItemEntity::are_mergeable(&stone(1), &stone(5)));
|
||||
assert!(ItemEntity::are_mergeable(&stone(32), &stone(32)));
|
||||
assert!(!ItemEntity::are_mergeable(&stone(33), &stone(32)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unstackable_items_never_merge() {
|
||||
let sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
|
||||
assert!(!ItemEntity::are_mergeable(&sword, &sword.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_components_never_merge() {
|
||||
let plain = ItemStack::new(1, &Item::STONE);
|
||||
|
||||
let mut named = plain.clone();
|
||||
named.set_data_component(CustomNameImpl {
|
||||
name: TextComponent::text("a"),
|
||||
});
|
||||
let mut renamed = plain.clone();
|
||||
renamed.set_data_component(CustomNameImpl {
|
||||
name: TextComponent::text("b"),
|
||||
});
|
||||
assert!(!ItemEntity::are_mergeable(&plain, &named));
|
||||
assert!(!ItemEntity::are_mergeable(&named, &renamed));
|
||||
assert!(ItemEntity::are_mergeable(&named, &named.clone()));
|
||||
|
||||
let mut enchanted = plain.clone();
|
||||
enchanted.add_enchantment(&Enchantment::UNBREAKING, 1);
|
||||
let mut stronger = plain.clone();
|
||||
stronger.add_enchantment(&Enchantment::UNBREAKING, 2);
|
||||
assert!(!ItemEntity::are_mergeable(&plain, &enchanted));
|
||||
assert!(!ItemEntity::are_mergeable(&enchanted, &stronger));
|
||||
|
||||
let mut data = NbtCompound::new();
|
||||
data.put_string("id", "a".to_string());
|
||||
let mut tagged = plain.clone();
|
||||
tagged.set_data_component(CustomDataImpl::new(data));
|
||||
assert!(!ItemEntity::are_mergeable(&plain, &tagged));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damage_resistant_items() {
|
||||
let sword = ItemStack::new(1, &Item::NETHERITE_SWORD);
|
||||
assert!(!ItemEntity::can_be_hurt_by(&sword, DamageType::LAVA));
|
||||
assert!(ItemEntity::can_be_hurt_by(&sword, DamageType::CACTUS));
|
||||
|
||||
let star = ItemStack::new(1, &Item::NETHER_STAR);
|
||||
assert!(!ItemEntity::can_be_hurt_by(&star, DamageType::EXPLOSION));
|
||||
assert!(ItemEntity::can_be_hurt_by(&star, DamageType::LAVA));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2925,7 +2925,7 @@ impl LivingEntity {
|
||||
let mut amount = amount;
|
||||
|
||||
// Check invulnerability before applying damage
|
||||
if self.entity.is_invulnerable_to(&damage_type) {
|
||||
if self.entity.is_invulnerable_to(&damage_type, cause) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -219,11 +219,8 @@ impl Mob for WitchEntity {
|
||||
}
|
||||
|
||||
fn modify_incoming_damage(&self, mut amount: f32, damage_type: DamageType) -> f32 {
|
||||
if damage_type == DamageType::MAGIC
|
||||
|| damage_type == DamageType::INDIRECT_MAGIC
|
||||
|| damage_type == DamageType::THORNS
|
||||
|| damage_type == DamageType::WITHER
|
||||
{
|
||||
// Vanilla `Witch.getDamageAfterMagicAbsorb`.
|
||||
if damage_type.has_tag(&tag::DamageType::MINECRAFT_WITCH_RESISTANT_TO) {
|
||||
amount *= 0.15;
|
||||
}
|
||||
amount
|
||||
|
||||
@@ -324,18 +324,17 @@ pub trait EntityBase: Send + Sync + std::any::Any {
|
||||
caller: &dyn EntityBase,
|
||||
lightning: &lightning::LightningBoltEntity,
|
||||
) {
|
||||
if self.get_living_entity().is_some() {
|
||||
self.set_on_fire_for(8.0);
|
||||
let cause = lightning.get_cause();
|
||||
self.damage_with_context(
|
||||
caller,
|
||||
5.0,
|
||||
DamageType::LIGHTNING_BOLT,
|
||||
None,
|
||||
Some(lightning),
|
||||
cause.as_deref().map(|p| p as &dyn EntityBase),
|
||||
);
|
||||
}
|
||||
// Vanilla `Entity.thunderHit` hits entities with 5 damage and sets them on fire for 8 seconds.
|
||||
self.set_on_fire_for(8.0);
|
||||
let cause = lightning.get_cause();
|
||||
self.damage_with_context(
|
||||
caller,
|
||||
5.0,
|
||||
DamageType::LIGHTNING_BOLT,
|
||||
None,
|
||||
Some(lightning),
|
||||
cause.as_deref().map(|p| p as &dyn EntityBase),
|
||||
);
|
||||
}
|
||||
|
||||
fn is_spectator(&self) -> bool {
|
||||
@@ -488,7 +487,7 @@ pub trait EntityBase: Send + Sync + std::any::Any {
|
||||
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) {
|
||||
if !entity.is_fire_immune() {
|
||||
self.set_on_fire_for_ticks((seconds * 20.0).floor() as u32);
|
||||
}
|
||||
}
|
||||
@@ -1714,6 +1713,17 @@ impl Entity {
|
||||
// updateWaterState() in yarn
|
||||
|
||||
fn update_fluid_state(&self, caller: &dyn EntityBase) {
|
||||
let world = self.world.load();
|
||||
for fluid in self.update_fluid_interaction(caller) {
|
||||
world
|
||||
.block_registry
|
||||
.on_entity_collision_fluid(fluid, caller);
|
||||
}
|
||||
}
|
||||
|
||||
/// Vanilla `Entity.updateFluidInteraction`: fluid flags and current push only, without the
|
||||
/// fluid's `entityInside` effects. Returns the fluids the entity is in.
|
||||
fn update_fluid_interaction(&self, caller: &dyn EntityBase) -> Vec<&'static Fluid> {
|
||||
let is_pushed = caller.is_pushed_by_fluids();
|
||||
let mut fluids = BTreeMap::new();
|
||||
|
||||
@@ -1788,14 +1798,6 @@ impl Entity {
|
||||
}
|
||||
}
|
||||
|
||||
// BTreeMap auto-sorts water before lava as in vanilla
|
||||
|
||||
for (_, fluid) in fluids {
|
||||
world
|
||||
.block_registry
|
||||
.on_entity_collision_fluid(fluid, caller);
|
||||
}
|
||||
|
||||
let lava_speed = if world.dimension.fast_lava {
|
||||
0.007
|
||||
} else {
|
||||
@@ -1840,6 +1842,9 @@ impl Entity {
|
||||
self.lava_height.store(lava_height);
|
||||
|
||||
self.touching_lava.store(in_lava, Ordering::SeqCst);
|
||||
|
||||
// BTreeMap auto-sorts water before lava as in vanilla
|
||||
fluids.into_values().collect()
|
||||
}
|
||||
|
||||
fn push_by_fluid(&self, speed: f64, mut push: Vector3<f64>, n: usize) {
|
||||
@@ -3049,26 +3054,38 @@ impl Entity {
|
||||
self.send_bedrock_actor_data(&bedrock_meta);
|
||||
}
|
||||
|
||||
/// Checks if the entity is invulnerable to the given damage type, considering both general invulnerability and specific immunities.
|
||||
pub 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;
|
||||
}
|
||||
/// Vanilla `Entity.fireImmune`: the entity type.
|
||||
pub fn is_fire_immune(&self) -> bool {
|
||||
self.entity_type.fire_immune || self.fire_immune.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
// General invulnerability
|
||||
if self.invulnerable.load(Ordering::Relaxed) {
|
||||
return true;
|
||||
}
|
||||
/// Vanilla `Entity.isInvulnerableToBase`. Driven by the damage type tags, so datapacks
|
||||
/// decide what bypasses invulnerability or counts as fire and fall damage.
|
||||
/// `cause` is the attacker (vanilla `DamageSource.getEntity`).
|
||||
pub fn is_invulnerable_to(
|
||||
&self,
|
||||
damage_type: &DamageType,
|
||||
cause: Option<&dyn EntityBase>,
|
||||
) -> bool {
|
||||
let bypasses = damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_INVULNERABILITY);
|
||||
let creative_cause = cause
|
||||
.and_then(EntityBase::get_player)
|
||||
.is_some_and(Player::is_creative);
|
||||
|
||||
// Specific type immunities
|
||||
self.damage_immunities
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.contains(damage_type)
|
||||
self.removed.load(Ordering::SeqCst)
|
||||
|| (self.invulnerable.load(Ordering::Relaxed) && !bypasses && !creative_cause)
|
||||
|| (damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE) && self.is_fire_immune())
|
||||
|| (damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FALL)
|
||||
&& self
|
||||
.entity_type
|
||||
.has_tag(&tag::EntityType::MINECRAFT_FALL_DAMAGE_IMMUNE))
|
||||
// Plugin immunities, which never block damage that bypasses invulnerability.
|
||||
|| (!bypasses
|
||||
&& self
|
||||
.damage_immunities
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.contains(damage_type))
|
||||
}
|
||||
|
||||
/// Sets if the entity is invulnerable to a specific damage type
|
||||
@@ -4142,7 +4159,7 @@ impl EntityBase for Entity {
|
||||
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);
|
||||
let is_immune = self.is_fire_immune();
|
||||
if fire_ticks > 0 {
|
||||
if is_immune {
|
||||
self.fire_ticks.store(fire_ticks - 4, Ordering::Relaxed);
|
||||
@@ -4150,7 +4167,8 @@ impl EntityBase for Entity {
|
||||
self.extinguish();
|
||||
}
|
||||
} else {
|
||||
if fire_ticks % 20 == 0 {
|
||||
// lava deals its own damage.
|
||||
if fire_ticks % 20 == 0 && !self.touching_lava.load(Ordering::SeqCst) {
|
||||
caller.damage(caller, 1.0, DamageType::ON_FIRE);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,3 +9,8 @@ fo = "fo"
|
||||
undescore = "undescore"
|
||||
replaceables = "replaceables"
|
||||
convertable = "convertable"
|
||||
|
||||
[default.extend-identifiers]
|
||||
# Vanilla method names
|
||||
areMergable = "areMergable"
|
||||
isMergable = "isMergable"
|
||||
|
||||
Reference in New Issue
Block a user