aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0243-Improve-death-events.patch
diff options
context:
space:
mode:
Diffstat (limited to 'patches/server/0243-Improve-death-events.patch')
-rw-r--r--patches/server/0243-Improve-death-events.patch512
1 files changed, 512 insertions, 0 deletions
diff --git a/patches/server/0243-Improve-death-events.patch b/patches/server/0243-Improve-death-events.patch
new file mode 100644
index 0000000000..13a1d5d493
--- /dev/null
+++ b/patches/server/0243-Improve-death-events.patch
@@ -0,0 +1,512 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Phoenix616 <[email protected]>
+Date: Tue, 21 Aug 2018 01:39:35 +0100
+Subject: [PATCH] Improve death events
+
+This adds the ability to cancel the death events and to modify the sound
+an entity makes when dying. (In cases were no sound should it will be
+called with shouldPlaySound set to false allowing unsilencing of silent
+entities)
+
+It makes handling of entity deaths a lot nicer as you no longer need
+to listen on the damage event and calculate if the entity dies yourself
+to cancel the death which has the benefit of also receiving the dropped
+items and experience which is otherwise only properly possible by using
+internal code.
+
+== AT ==
+public net.minecraft.world.entity.LivingEntity getDeathSound()Lnet/minecraft/sounds/SoundEvent;
+public net.minecraft.world.entity.LivingEntity getSoundVolume()F
+
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index 00a1a570e9f7abf97a25b4bab2b7532d3dad7912..0706dd82cfdb6808ef61149b2a8d8be971f19be9 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -268,6 +268,10 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ private int containerCounter;
+ public boolean wonGame;
+ private int containerUpdateDelay; // Paper - Configurable container update tick rate
++ // Paper start - cancellable death event
++ public boolean queueHealthUpdatePacket;
++ public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
++ // Paper end - cancellable death event
+
+ // CraftBukkit start
+ public CraftPlayer.TransferCookieConnection transferCookieConnection;
+@@ -893,7 +897,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+
+ @Override
+ public void die(DamageSource damageSource) {
+- this.gameEvent(GameEvent.ENTITY_DIE);
++ // this.gameEvent(GameEvent.ENTITY_DIE); // Paper - move below event cancellation check
+ boolean flag = this.level().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES);
+ // CraftBukkit start - fire PlayerDeathEvent
+ if (this.isRemoved()) {
+@@ -921,6 +925,16 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ String deathmessage = defaultMessage.getString();
+ this.keepLevel = keepInventory; // SPIGOT-2222: pre-set keepLevel
+ org.bukkit.event.entity.PlayerDeathEvent event = CraftEventFactory.callPlayerDeathEvent(this, damageSource, loot, PaperAdventure.asAdventure(defaultMessage), keepInventory); // Paper - Adventure
++ // Paper start - cancellable death event
++ if (event.isCancelled()) {
++ // make compatible with plugins that might have already set the health in an event listener
++ if (this.getHealth() <= 0) {
++ this.setHealth((float) event.getReviveHealth());
++ }
++ return;
++ }
++ this.gameEvent(GameEvent.ENTITY_DIE); // moved from the top of this method
++ // Paper end
+
+ // SPIGOT-943 - only call if they have an inventory open
+ if (this.containerMenu != this.inventoryMenu) {
+@@ -1069,8 +1083,17 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player {
+ }
+ }
+ }
+-
+- return super.hurt(source, amount);
++ // Paper start - cancellable death events
++ //return super.hurt(source, amount);
++ this.queueHealthUpdatePacket = true;
++ boolean damaged = super.hurt(source, amount);
++ this.queueHealthUpdatePacket = false;
++ if (this.queuedHealthUpdatePacket != null) {
++ this.connection.send(this.queuedHealthUpdatePacket);
++ this.queuedHealthUpdatePacket = null;
++ }
++ return damaged;
++ // Paper end
+ }
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+index f7a77b31dc196823510f96bd3b2344058e20feac..279fa00fd9043e1995f22c79f47d0b41c27bd933 100644
+--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
++++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
+@@ -283,6 +283,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ public Set<UUID> collidableExemptions = new HashSet<>();
+ public boolean bukkitPickUpLoot;
+ public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
++ public boolean silentDeath = false; // Paper - mark entity as dying silently for cancellable death event
+
+ @Override
+ public float getBukkitYaw() {
+@@ -1537,11 +1538,12 @@ public abstract class LivingEntity extends Entity implements Attackable {
+
+ if (this.isDeadOrDying()) {
+ if (!this.checkTotemDeathProtection(source)) {
+- if (flag1) {
+- this.makeSound(this.getDeathSound());
+- }
++ // Paper start - moved into CraftEventFactory event caller for cancellable death event
++ this.silentDeath = !flag1; // mark entity as dying silently
++ // Paper end
+
+ this.die(source);
++ this.silentDeath = false; // Paper - cancellable death event - reset to default
+ }
+ } else if (flag1) {
+ this.playHurtSound(source);
+@@ -1700,6 +1702,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ Entity entity = damageSource.getEntity();
+ LivingEntity entityliving = this.getKillCredit();
+
++ /* // Paper - move down to make death event cancellable - this is the awardKillScore below
+ if (this.deathScore >= 0 && entityliving != null) {
+ entityliving.awardKillScore(this, this.deathScore, damageSource);
+ }
+@@ -1711,24 +1714,59 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ if (!this.level().isClientSide && this.hasCustomName()) {
+ if (org.spigotmc.SpigotConfig.logNamedDeaths) LivingEntity.LOGGER.info("Named entity {} died: {}", this, this.getCombatTracker().getDeathMessage().getString()); // Spigot
+ }
++ */ // Paper - move down to make death event cancellable - this is the awardKillScore below
+
+ this.dead = true;
+- this.getCombatTracker().recheckStatus();
++ // Paper - moved into if below
+ Level world = this.level();
+
+ if (world instanceof ServerLevel) {
+ ServerLevel worldserver = (ServerLevel) world;
++ // Paper - move below into if for onKill
++
++ // Paper start
++ org.bukkit.event.entity.EntityDeathEvent deathEvent = this.dropAllDeathLoot(worldserver, damageSource);
++ if (deathEvent == null || !deathEvent.isCancelled()) {
++ if (this.deathScore >= 0 && entityliving != null) {
++ entityliving.awardKillScore(this, this.deathScore, damageSource);
++ }
++ // Paper start - clear equipment if event is not cancelled
++ if (this instanceof Mob) {
++ for (EquipmentSlot slot : this.clearedEquipmentSlots) {
++ this.setItemSlot(slot, ItemStack.EMPTY);
++ }
++ this.clearedEquipmentSlots.clear();
++ }
++ // Paper end
++
++ if (this.isSleeping()) {
++ this.stopSleeping();
++ }
+
+- if (entity == null || entity.killedEntity(worldserver, this)) {
++ if (!this.level().isClientSide && this.hasCustomName()) {
++ if (org.spigotmc.SpigotConfig.logNamedDeaths) LivingEntity.LOGGER.info("Named entity {} died: {}", this, this.getCombatTracker().getDeathMessage().getString()); // Spigot
++ }
++
++ this.getCombatTracker().recheckStatus();
++ if (entity != null) {
++ entity.killedEntity((ServerLevel) this.level(), this);
++ }
+ this.gameEvent(GameEvent.ENTITY_DIE);
+- this.dropAllDeathLoot(worldserver, damageSource);
++ } else {
++ this.dead = false;
++ this.setHealth((float) deathEvent.getReviveHealth());
++ }
++ // Paper end
+ this.createWitherRose(entityliving);
+ }
+
++ // Paper start
++ if (this.dead) { // Paper
+ this.level().broadcastEntityEvent(this, (byte) 3);
+- }
+
+ this.setPose(Pose.DYING);
++ }
++ // Paper end
+ }
+ }
+
+@@ -1736,7 +1774,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ if (!this.level().isClientSide) {
+ boolean flag = false;
+
+- if (adversary instanceof WitherBoss) {
++ if (this.dead && adversary instanceof WitherBoss) { // Paper
+ if (this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) {
+ BlockPos blockposition = this.blockPosition();
+ BlockState iblockdata = Blocks.WITHER_ROSE.defaultBlockState();
+@@ -1765,24 +1803,37 @@ public abstract class LivingEntity extends Entity implements Attackable {
+ }
+ }
+
+- protected void dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
++ // Paper start
++ protected boolean clearEquipmentSlots = true;
++ protected Set<EquipmentSlot> clearedEquipmentSlots = new java.util.HashSet<>();
++ protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
++ // Paper end
+ boolean flag = this.lastHurtByPlayerTime > 0;
+
+ this.dropEquipment(); // CraftBukkit - from below
+ if (this.shouldDropLoot() && world.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) {
+ this.dropFromLootTable(damageSource, flag);
++ // Paper start
++ final boolean prev = this.clearEquipmentSlots;
++ this.clearEquipmentSlots = false;
++ this.clearedEquipmentSlots.clear();
++ // Paper end
+ this.dropCustomDeathLoot(world, damageSource, flag);
++ this.clearEquipmentSlots = prev; // Paper
+ }
+ // CraftBukkit start - Call death event
+- CraftEventFactory.callEntityDeathEvent(this, damageSource, this.drops);
++ org.bukkit.event.entity.EntityDeathEvent deathEvent = CraftEventFactory.callEntityDeathEvent(this, damageSource, this.drops); // Paper
++ this.postDeathDropItems(deathEvent); // Paper
+ this.drops = new ArrayList<>();
+ // CraftBukkit end
+
+ // this.dropEquipment();// CraftBukkit - moved up
+ this.dropExperience(damageSource.getEntity());
++ return deathEvent; // Paper
+ }
+
+ protected void dropEquipment() {}
++ protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {} // Paper - method for post death logic that cannot be ran before the event is potentially cancelled
+
+ public int getExpReward(@Nullable Entity entity) { // CraftBukkit
+ Level world = this.level();
+diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
+index b7c216b79684a4dbb93899fd2d3bc5a9e1b04f2e..4b0e269f3580c1c6dac1e5f2dd3cdac1d8e1118a 100644
+--- a/src/main/java/net/minecraft/world/entity/Mob.java
++++ b/src/main/java/net/minecraft/world/entity/Mob.java
+@@ -1121,6 +1121,12 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+
+ }
+
++ // Paper start
++ protected boolean shouldSkipLoot(EquipmentSlot slot) { // method to avoid to fallback into the global mob loot logic (i.e fox)
++ return false;
++ }
++ // Paper end
++
+ @Override
+ protected void dropCustomDeathLoot(ServerLevel world, DamageSource source, boolean causedByPlayer) {
+ super.dropCustomDeathLoot(world, source, causedByPlayer);
+@@ -1129,6 +1135,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+
+ for (int j = 0; j < i; ++j) {
+ EquipmentSlot enumitemslot = aenumitemslot[j];
++ if (this.shouldSkipLoot(enumitemslot)) continue; // Paper
+ ItemStack itemstack = this.getItemBySlot(enumitemslot);
+ float f = this.getEquipmentDropChance(enumitemslot);
+
+@@ -1153,7 +1160,13 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
+ }
+
+ this.spawnAtLocation(itemstack);
++ if (this.clearEquipmentSlots) { // Paper
+ this.setItemSlot(enumitemslot, ItemStack.EMPTY);
++ // Paper start
++ } else {
++ this.clearedEquipmentSlots.add(enumitemslot);
++ }
++ // Paper end
+ }
+ }
+ }
+diff --git a/src/main/java/net/minecraft/world/entity/animal/Fox.java b/src/main/java/net/minecraft/world/entity/animal/Fox.java
+index a6788da1505f9e119c03b94488f5e006da13e918..e46c8231ee318eda0512afbb6343b426b4838643 100644
+--- a/src/main/java/net/minecraft/world/entity/animal/Fox.java
++++ b/src/main/java/net/minecraft/world/entity/animal/Fox.java
+@@ -704,16 +704,38 @@ public class Fox extends Animal implements VariantHolder<Fox.Type> {
+ return this.getTrustedUUIDs().contains(uuid);
+ }
+
++ // Paper start - handle the bitten item separately like vanilla
+ @Override
+- protected void dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
++ protected boolean shouldSkipLoot(EquipmentSlot slot) {
++ return slot == EquipmentSlot.MAINHAND;
++ }
++ // Paper end
++
++ @Override
++ // Paper start - Cancellable death event
++ protected org.bukkit.event.entity.EntityDeathEvent dropAllDeathLoot(ServerLevel world, DamageSource damageSource) {
+ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.MAINHAND);
+
+- if (!itemstack.isEmpty()) {
++ boolean releaseMouth = false;
++ if (!itemstack.isEmpty() && this.level().getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { // Fix MC-153010
+ this.spawnAtLocation(itemstack);
++ releaseMouth = true;
++ }
++
++ org.bukkit.event.entity.EntityDeathEvent deathEvent = super.dropAllDeathLoot(world, damageSource);
++
++ // Below is code to drop
++
++ if (deathEvent == null || deathEvent.isCancelled()) {
++ return deathEvent;
++ }
++
++ if (releaseMouth) {
++ // Paper end - Cancellable death event
+ this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
+ }
+
+- super.dropAllDeathLoot(world, damageSource);
++ return deathEvent; // Paper - Cancellable death event
+ }
+
+ public static boolean isPathClear(Fox fox, LivingEntity chasedEntity) {
+diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
+index 767817fb1418958c89d0db9da4ae7eb8a5a16076..5654c614f07f07ff642ba4851b0cb6fa185924ae 100644
+--- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
++++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java
+@@ -71,9 +71,17 @@ public abstract class AbstractChestedHorse extends AbstractHorse {
+ this.spawnAtLocation(Blocks.CHEST);
+ }
+
++ //this.setChest(false); // Paper - moved to post death logic
++ }
++ }
++
++ // Paper start
++ protected void postDeathDropItems(org.bukkit.event.entity.EntityDeathEvent event) {
++ if (this.hasChest() && (event == null || !event.isCancelled())) {
+ this.setChest(false);
+ }
+ }
++ // Paper end
+
+ @Override
+ public void addAdditionalSaveData(CompoundTag nbt) {
+diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
+index ee3902cbada46ffb78c42dbf6f00c859546c76e1..92bb0c63330ad3a4cb13b2dc655020714e9b1ffd 100644
+--- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
++++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java
+@@ -505,8 +505,10 @@ public class ArmorStand extends LivingEntity {
+ }
+ // CraftBukkit end
+ if (source.is(DamageTypeTags.IS_EXPLOSION)) {
+- this.brokenByAnything(worldserver, source);
+- this.kill(source); // CraftBukkit
++ // Paper start - avoid duplicate event call
++ org.bukkit.event.entity.EntityDeathEvent event = this.brokenByAnything(worldserver, source);
++ if (!event.isCancelled()) this.kill(source, false); // CraftBukkit
++ // Paper end
+ return false;
+ } else if (source.is(DamageTypeTags.IGNITES_ARMOR_STANDS)) {
+ if (this.isOnFire()) {
+@@ -549,9 +551,9 @@ public class ArmorStand extends LivingEntity {
+ this.gameEvent(GameEvent.ENTITY_DAMAGE, source.getEntity());
+ this.lastHit = i;
+ } else {
+- this.brokenByPlayer(worldserver, source);
++ org.bukkit.event.entity.EntityDeathEvent event = this.brokenByPlayer(worldserver, source); // Paper
+ this.showBreakingParticles();
+- this.discard(EntityRemoveEvent.Cause.DEATH); // CraftBukkit - SPIGOT-4890: remain as this.discard() since above damagesource method will call death event
++ if (!event.isCancelled()) this.kill(source, false); // Paper - we still need to kill to follow vanilla logic (emit the game event etc...)
+ }
+
+ return true;
+@@ -604,8 +606,10 @@ public class ArmorStand extends LivingEntity {
+
+ f1 -= amount;
+ if (f1 <= 0.5F) {
+- this.brokenByAnything(world, damageSource);
+- this.kill(damageSource); // CraftBukkit
++ // Paper start - avoid duplicate event call
++ org.bukkit.event.entity.EntityDeathEvent event = this.brokenByAnything(world, damageSource);
++ if (!event.isCancelled()) this.kill(damageSource, false); // CraftBukkit
++ // Paper end
+ } else {
+ this.setHealth(f1);
+ this.gameEvent(GameEvent.ENTITY_DAMAGE, damageSource.getEntity());
+@@ -613,15 +617,15 @@ public class ArmorStand extends LivingEntity {
+
+ }
+
+- private void brokenByPlayer(ServerLevel world, DamageSource damageSource) {
++ private org.bukkit.event.entity.EntityDeathEvent brokenByPlayer(ServerLevel world, DamageSource damageSource) { // Paper
+ ItemStack itemstack = new ItemStack(Items.ARMOR_STAND);
+
+ itemstack.set(DataComponents.CUSTOM_NAME, this.getCustomName());
+ this.drops.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(itemstack)); // CraftBukkit - add to drops
+- this.brokenByAnything(world, damageSource);
++ return this.brokenByAnything(world, damageSource); // Paper
+ }
+
+- private void brokenByAnything(ServerLevel world, DamageSource damageSource) {
++ private org.bukkit.event.entity.EntityDeathEvent brokenByAnything(ServerLevel world, DamageSource damageSource) { // Paper
+ this.playBrokenSound();
+ // this.dropAllDeathLoot(worldserver, damagesource); // CraftBukkit - moved down
+
+@@ -643,7 +647,7 @@ public class ArmorStand extends LivingEntity {
+ this.armorItems.set(i, ItemStack.EMPTY);
+ }
+ }
+- this.dropAllDeathLoot(world, damageSource); // CraftBukkit - moved from above
++ return this.dropAllDeathLoot(world, damageSource); // CraftBukkit - moved from above // Paper
+
+ }
+
+@@ -770,7 +774,15 @@ public class ArmorStand extends LivingEntity {
+ }
+
+ public void kill(DamageSource damageSource) {
+- org.bukkit.craftbukkit.event.CraftEventFactory.callEntityDeathEvent(this, (damageSource == null ? this.damageSources().genericKill() : damageSource), this.drops); // CraftBukkit - call event
++ // Paper start - make cancellable
++ this.kill(damageSource, true);
++ }
++ public void kill(DamageSource damageSource, boolean callEvent) {
++ if (callEvent) {
++ org.bukkit.event.entity.EntityDeathEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callEntityDeathEvent(this, (damageSource == null ? this.damageSources().genericKill() : damageSource), this.drops); // CraftBukkit - call event
++ if (event.isCancelled()) return;
++ }
++ // Paper end
+ this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+ // CraftBukkit end
+ this.gameEvent(GameEvent.ENTITY_DIE);
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 597594409b4d1fadf4ae1c3a7156421e70989d97..2d1e0e92e6d98a9cf597c3717c36ea5a337577c3 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -2517,7 +2517,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ @Override
+ public void sendHealthUpdate() {
+ FoodData foodData = this.getHandle().getFoodData();
+- this.sendHealthUpdate(this.getScaledHealth(), foodData.getFoodLevel(), foodData.getSaturationLevel());
++ // Paper start - cancellable death event
++ ClientboundSetHealthPacket packet = new ClientboundSetHealthPacket(this.getScaledHealth(), foodData.getFoodLevel(), foodData.getSaturationLevel());
++ if (this.getHandle().queueHealthUpdatePacket) {
++ this.getHandle().queuedHealthUpdatePacket = packet;
++ } else {
++ this.getHandle().connection.send(packet);
++ }
++ // Paper end
+ }
+
+ public void injectScaledMaxHealth(Collection<AttributeInstance> collection, boolean force) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+index 1d2e9f3e5e232faca8de4760d3574fae6200b2b2..e7ba5b503e821d18467c2300f780ef37f996b34d 100644
+--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
+@@ -894,9 +894,16 @@ public class CraftEventFactory {
+ CraftLivingEntity entity = (CraftLivingEntity) victim.getBukkitEntity();
+ CraftDamageSource bukkitDamageSource = new CraftDamageSource(damageSource);
+ EntityDeathEvent event = new EntityDeathEvent(entity, bukkitDamageSource, drops, victim.getExpReward(damageSource.getEntity()));
++ populateFields(victim, event); // Paper - make cancellable
+ CraftWorld world = (CraftWorld) entity.getWorld();
+ Bukkit.getServer().getPluginManager().callEvent(event);
+
++ // Paper start - make cancellable
++ if (event.isCancelled()) {
++ return event;
++ }
++ playDeathSound(victim, event);
++ // Paper end
+ victim.expToDrop = event.getDroppedExp();
+
+ for (org.bukkit.inventory.ItemStack stack : event.getDrops()) {
+@@ -914,7 +921,14 @@ public class CraftEventFactory {
+ PlayerDeathEvent event = new PlayerDeathEvent(entity, bukkitDamageSource, drops, victim.getExpReward(damageSource.getEntity()), 0, deathMessage);
+ event.setKeepInventory(keepInventory);
+ event.setKeepLevel(victim.keepLevel); // SPIGOT-2222: pre-set keepLevel
++ populateFields(victim, event); // Paper - make cancellable
+ Bukkit.getServer().getPluginManager().callEvent(event);
++ // Paper start - make cancellable
++ if (event.isCancelled()) {
++ return event;
++ }
++ playDeathSound(victim, event);
++ // Paper end
+
+ victim.keepLevel = event.getKeepLevel();
+ victim.newLevel = event.getNewLevel();
+@@ -931,6 +945,31 @@ public class CraftEventFactory {
+ return event;
+ }
+
++ // Paper start - helper methods for making death event cancellable
++ // Add information to death event
++ private static void populateFields(net.minecraft.world.entity.LivingEntity victim, EntityDeathEvent event) {
++ event.setReviveHealth(event.getEntity().getAttribute(org.bukkit.attribute.Attribute.GENERIC_MAX_HEALTH).getValue());
++ event.setShouldPlayDeathSound(!victim.silentDeath && !victim.isSilent());
++ net.minecraft.sounds.SoundEvent soundEffect = victim.getDeathSound();
++ event.setDeathSound(soundEffect != null ? org.bukkit.craftbukkit.CraftSound.minecraftToBukkit(soundEffect) : null);
++ event.setDeathSoundCategory(org.bukkit.SoundCategory.valueOf(victim.getSoundSource().name()));
++ event.setDeathSoundVolume(victim.getSoundVolume());
++ event.setDeathSoundPitch(victim.getVoicePitch());
++ }
++
++ // Play death sound manually
++ private static void playDeathSound(net.minecraft.world.entity.LivingEntity victim, EntityDeathEvent event) {
++ if (event.shouldPlayDeathSound() && event.getDeathSound() != null && event.getDeathSoundCategory() != null) {
++ net.minecraft.world.entity.player.Player source = victim instanceof net.minecraft.world.entity.player.Player ? (net.minecraft.world.entity.player.Player) victim : null;
++ double x = event.getEntity().getLocation().getX();
++ double y = event.getEntity().getLocation().getY();
++ double z = event.getEntity().getLocation().getZ();
++ net.minecraft.sounds.SoundEvent soundEffect = org.bukkit.craftbukkit.CraftSound.bukkitToMinecraft(event.getDeathSound());
++ net.minecraft.sounds.SoundSource soundCategory = net.minecraft.sounds.SoundSource.valueOf(event.getDeathSoundCategory().name());
++ victim.level().playSound(source, x, y, z, soundEffect, soundCategory, event.getDeathSoundVolume(), event.getDeathSoundPitch());
++ }
++ }
++ // Paper end
+ /**
+ * Server methods
+ */