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:
Zinedin Polic
2026-10-05 17:08:58 +02:00
committed by GitHub
parent 93f63c33c5
commit 86683caa41
9 changed files with 378 additions and 227 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -9,3 +9,8 @@ fo = "fo"
undescore = "undescore"
replaceables = "replaceables"
convertable = "convertable"
[default.extend-identifiers]
# Vanilla method names
areMergable = "areMergable"
isMergable = "isMergable"