diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerArmorChangeEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerArmorChangeEvent.java
index c7cc612ec..f146a8cc9 100644
--- a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerArmorChangeEvent.java
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerArmorChangeEvent.java
@@ -5,6 +5,7 @@ import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
+import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
@@ -16,8 +17,10 @@ import static org.bukkit.Material.*;
* Called when the player themselves change their armor items
*
* Not currently called for environmental factors though it MAY BE IN THE FUTURE
+ * @apiNote Use {@link io.papermc.paper.event.entity.EntityEquipmentChangedEvent} for all entity equipment changes
*/
@NullMarked
+@ApiStatus.Obsolete(since = "1.21.4")
public class PlayerArmorChangeEvent extends PlayerEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
@@ -38,11 +41,27 @@ public class PlayerArmorChangeEvent extends PlayerEvent {
* Gets the type of slot being altered.
*
* @return type of slot being altered
+ * @deprecated {@link SlotType} does not accurately represent what item types are valid in each slot. Use {@link #getSlot()} instead.
*/
+ @Deprecated(since = "1.21.4")
public SlotType getSlotType() {
return this.slotType;
}
+ /**
+ * Gets the slot being altered.
+ *
+ * @return slot being altered
+ */
+ public EquipmentSlot getSlot() {
+ return switch (this.slotType) {
+ case HEAD -> EquipmentSlot.HEAD;
+ case CHEST -> EquipmentSlot.CHEST;
+ case LEGS -> EquipmentSlot.LEGS;
+ case FEET -> EquipmentSlot.FEET;
+ };
+ }
+
/**
* Gets the existing item that's being replaced
*
@@ -70,6 +89,10 @@ public class PlayerArmorChangeEvent extends PlayerEvent {
return HANDLER_LIST;
}
+ /**
+ * @deprecated {@link SlotType} does not accurately represent what item types are valid in each slot.
+ */
+ @Deprecated(since = "1.21.4")
public enum SlotType {
HEAD(NETHERITE_HELMET, DIAMOND_HELMET, GOLDEN_HELMET, IRON_HELMET, CHAINMAIL_HELMET, LEATHER_HELMET, CARVED_PUMPKIN, PLAYER_HEAD, SKELETON_SKULL, ZOMBIE_HEAD, CREEPER_HEAD, WITHER_SKELETON_SKULL, TURTLE_HELMET, DRAGON_HEAD, PIGLIN_HEAD),
CHEST(NETHERITE_CHESTPLATE, DIAMOND_CHESTPLATE, GOLDEN_CHESTPLATE, IRON_CHESTPLATE, CHAINMAIL_CHESTPLATE, LEATHER_CHESTPLATE, ELYTRA),
diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/EntityEquipmentChangedEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityEquipmentChangedEvent.java
new file mode 100644
index 000000000..ebb31c12d
--- /dev/null
+++ b/paper-api/src/main/java/io/papermc/paper/event/entity/EntityEquipmentChangedEvent.java
@@ -0,0 +1,89 @@
+package io.papermc.paper.event.entity;
+
+import java.util.Collections;
+import java.util.Map;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.entity.EntityEvent;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.Unmodifiable;
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Called whenever a change to an entity's equipment has been detected. This event is called after effects from
+ * attribute modifiers and enchantments have been updated.
+ *
+ * Examples of actions that can trigger this event:
+ *
+ * - An entity being added to a world.
+ * - A player logging in.
+ * - The durability of an equipment item changing.
+ * - A dispenser equipping an item onto an entity.
+ * - An entity picking up an armor or weapon item from the ground.
+ * - A player changing their equipped armor.
+ * - A player changes their currently held item.
+ *
+ */
+@NullMarked
+public class EntityEquipmentChangedEvent extends EntityEvent {
+
+ private static final HandlerList HANDLER_LIST = new HandlerList();
+
+ private final Map equipmentChanges;
+
+ @ApiStatus.Internal
+ public EntityEquipmentChangedEvent(final LivingEntity entity, final Map equipmentChanges) {
+ super(entity);
+
+ this.equipmentChanges = equipmentChanges;
+ }
+
+ @Override
+ public LivingEntity getEntity() {
+ return (LivingEntity) this.entity;
+ }
+
+ /**
+ * Gets a map of changed slots to their respective equipment changes.
+ *
+ * @return the equipment changes map
+ */
+ public @Unmodifiable Map getEquipmentChanges() {
+ return Collections.unmodifiableMap(this.equipmentChanges);
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLER_LIST;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLER_LIST;
+ }
+
+ /**
+ * Represents a change in equipment for a single equipment slot.
+ */
+ @ApiStatus.NonExtendable
+ public interface EquipmentChange {
+
+ /**
+ * Gets the existing item that is being replaced.
+ *
+ * @return the existing item
+ */
+ @Contract(pure = true, value = "-> new")
+ ItemStack oldItem();
+
+ /**
+ * Gets the new item that is replacing the existing item.
+ *
+ * @return the new item
+ */
+ @Contract(pure = true, value = "-> new")
+ ItemStack newItem();
+ }
+}
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
index 8015fbab2..862afaa4e 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
@@ -1336,20 +1336,57 @@
Map map = this.collectEquipmentChanges();
if (map != null) {
this.handleHandSwap(map);
-@@ -2595,6 +_,13 @@
+@@ -2586,6 +_,20 @@
+ @Nullable
+ private Map collectEquipmentChanges() {
+ Map map = null;
++ // Paper start - EntityEquipmentChangedEvent
++ record EquipmentChangeImpl(org.bukkit.inventory.ItemStack oldItem, org.bukkit.inventory.ItemStack newItem) implements io.papermc.paper.event.entity.EntityEquipmentChangedEvent.EquipmentChange {
++ @Override
++ public org.bukkit.inventory.ItemStack oldItem() {
++ return this.oldItem.clone();
++ }
++
++ @Override
++ public org.bukkit.inventory.ItemStack newItem() {
++ return this.newItem.clone();
++ }
++ }
++ Map equipmentChanges = null;
++ // Paper end - EntityEquipmentChangedEvent
+
+ for (EquipmentSlot equipmentSlot : EquipmentSlot.VALUES) {
+ ItemStack itemStack = switch (equipmentSlot.getType()) {
+@@ -2595,11 +_,20 @@
};
ItemStack itemBySlot = this.getItemBySlot(equipmentSlot);
if (this.equipmentHasChanged(itemStack, itemBySlot)) {
-+ // Paper start - PlayerArmorChangeEvent
++ // Paper start - EntityEquipmentChangedEvent, PlayerArmorChangeEvent
++ final org.bukkit.inventory.ItemStack oldItem = CraftItemStack.asBukkitCopy(itemStack);
++ final org.bukkit.inventory.ItemStack newItem = CraftItemStack.asBukkitCopy(itemBySlot);
+ if (this instanceof ServerPlayer && equipmentSlot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR) {
-+ final org.bukkit.inventory.ItemStack oldItem = CraftItemStack.asBukkitCopy(itemStack);
-+ final org.bukkit.inventory.ItemStack newItem = CraftItemStack.asBukkitCopy(itemBySlot);
+ new com.destroystokyo.paper.event.player.PlayerArmorChangeEvent((org.bukkit.entity.Player) this.getBukkitEntity(), com.destroystokyo.paper.event.player.PlayerArmorChangeEvent.SlotType.valueOf(equipmentSlot.name()), oldItem, newItem).callEvent();
+ }
-+ // Paper end - PlayerArmorChangeEvent
++ // Paper end - EntityEquipmentChangedEvent, PlayerArmorChangeEvent
if (map == null) {
map = Maps.newEnumMap(EquipmentSlot.class);
++ equipmentChanges = Maps.newEnumMap(org.bukkit.inventory.EquipmentSlot.class); // Paper - EntityEquipmentChangedEvent
}
+
+ map.put(equipmentSlot, itemBySlot);
++ equipmentChanges.put(org.bukkit.craftbukkit.CraftEquipmentSlot.getSlot(equipmentSlot), new EquipmentChangeImpl(oldItem, newItem)); // Paper - EntityEquipmentChangedEvent
+ AttributeMap attributes = this.getAttributes();
+ if (!itemStack.isEmpty()) {
+ this.stopLocationBasedEffects(itemStack, equipmentSlot, attributes);
+@@ -2624,6 +_,8 @@
+ }
+ }
+ }
++
++ new io.papermc.paper.event.entity.EntityEquipmentChangedEvent(this.getBukkitLivingEntity(), equipmentChanges).callEvent(); // Paper - EntityEquipmentChangedEvent
+ }
+
+ return map;
@@ -2664,7 +_,7 @@
this.lastBodyItemStack = itemStack;
}