From a1cb6c43d200c97a09df634ffe149484aafde08a Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Sat, 28 Sep 2024 11:05:45 -0700
Subject: [PATCH] Improve entity effect API (#11444)

---
 patches/api/Improve-entity-effect-API.patch   | 140 ++++++++++++++++
 patches/server/Bandaid-fix-for-Effect.patch   |   1 -
 .../server/Improve-entity-effect-API.patch    | 152 ++++++++++++++++++
 3 files changed, 292 insertions(+), 1 deletion(-)
 create mode 100644 patches/api/Improve-entity-effect-API.patch
 create mode 100644 patches/server/Improve-entity-effect-API.patch

diff --git a/patches/api/Improve-entity-effect-API.patch b/patches/api/Improve-entity-effect-API.patch
new file mode 100644
index 0000000000..c1c2fe5789
--- /dev/null
+++ b/patches/api/Improve-entity-effect-API.patch
@@ -0,0 +1,140 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Fri, 27 Sep 2024 17:12:50 -0700
+Subject: [PATCH] Improve entity effect API
+
+
+diff --git a/src/main/java/org/bukkit/EntityEffect.java b/src/main/java/org/bukkit/EntityEffect.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/EntityEffect.java
++++ b/src/main/java/org/bukkit/EntityEffect.java
+@@ -0,0 +0,0 @@ public enum EntityEffect {
+     // 5 - unused
+     /**
+      * The smoke when taming an entity fails.
++     * @deprecated use {@link EntityEffect#TAMING_FAILED}
+      */
++    @Deprecated(since = "1.21") // Paper
+     WOLF_SMOKE(6, Tameable.class),
++    // Paper start - rename "wolf" effects
++    /**
++     * The smoke when taming an entity fails.
++     */
++    TAMING_FAILED(6, Tameable.class),
++    /**
++     * The hearts when taming an entity succeeds.
++     */
++    TAMING_SUCCEEDED(7, Tameable.class),
++    // Paper end - rename "wolf" effects
+     /**
+      * The hearts when taming an entity succeeds.
++     * @deprecated use {@link EntityEffect#TAMING_SUCCEEDED}
+      */
++    @Deprecated(since = "1.21") // Paper
+     WOLF_HEARTS(7, Tameable.class),
+     /**
+      * When a wolf shakes (after being wet).
+@@ -0,0 +0,0 @@ public enum EntityEffect {
+     ARMOR_STAND_HIT(32, ArmorStand.class),
+     /**
+      * Entity hurt by thorns attack.
++     * @deprecated in favor of {@link LivingEntity#playHurtAnimation(float)} or {@link Entity#broadcastHurtAnimation(java.util.Collection)}
+      */
++    @Deprecated(since = "1.19.4", forRemoval = true) // Paper
+     THORNS_HURT(33, LivingEntity.class),
+     /**
+      * Iron golem puts away rose.
+@@ -0,0 +0,0 @@ public enum EntityEffect {
+     TOTEM_RESURRECT(35, LivingEntity.class),
+     /**
+      * Entity hurt due to drowning damage.
++     * @deprecated in favor of {@link LivingEntity#playHurtAnimation(float)} or {@link Entity#broadcastHurtAnimation(java.util.Collection)}
+      */
++    @Deprecated(since = "1.19.4", forRemoval = true)
+     HURT_DROWN(36, LivingEntity.class),
+     /**
+      * Entity hurt due to explosion damage.
++     * @deprecated in favor of {@link LivingEntity#playHurtAnimation(float)} or {@link Entity#broadcastHurtAnimation(java.util.Collection)}
+      */
++    @Deprecated(since = "1.19.4", forRemoval = true)
+     HURT_EXPLOSION(37, LivingEntity.class),
+     /**
+      * Dolphin has been fed and is locating a structure.
+@@ -0,0 +0,0 @@ public enum EntityEffect {
+     VILLAGER_SPLASH(42, Villager.class),
+     /**
+      * Player's bad omen effect removed to start or increase raid difficult.
++     * @deprecated raid system was overhauled in 1.20.5
+      */
++    @Deprecated(since = "1.20.5", forRemoval = true)
+     PLAYER_BAD_OMEN_RAID(43, Player.class),
+     /**
+      * Entity hurt due to berry bush. Prickly!
++     * @deprecated in favor of {@link LivingEntity#playHurtAnimation(float)} or {@link Entity#broadcastHurtAnimation(java.util.Collection)}
+      */
++    @Deprecated(since = "1.19.4", forRemoval = true)
+     HURT_BERRY_BUSH(44, LivingEntity.class),
+     /**
+      * Fox chews the food in its mouth
+@@ -0,0 +0,0 @@ public enum EntityEffect {
+      * Sniffer must have a target and be in {@link Sniffer.State#SEARCHING} or
+      * {@link Sniffer.State#DIGGING}
+      */
+-    SNIFFER_DIG(63, Sniffer.class);
++    SNIFFER_DIG(63, Sniffer.class),
++    // Paper start - add missing EntityEffect
++    /**
++     * Armadillo peeks out of its shell
++     */
++    ARMADILLO_PEEK(64, org.bukkit.entity.Armadillo.class),
++    /**
++     * {@link org.bukkit.inventory.EquipmentSlot#BODY} armor piece breaks
++     */
++    BODY_BREAK(65, LivingEntity.class);
++    // Paper end - add missing EntityEffect
+ 
+     private final byte data;
+     private final Class<? extends Entity> applicable;
+diff --git a/src/main/java/org/bukkit/entity/Entity.java b/src/main/java/org/bukkit/entity/Entity.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/entity/Entity.java
++++ b/src/main/java/org/bukkit/entity/Entity.java
+@@ -0,0 +0,0 @@ public interface Entity extends Metadatable, CommandSender, Nameable, Persistent
+      */
+     @NotNull String getScoreboardEntryName();
+     // Paper end - entity scoreboard name
++
++    // Paper start - broadcast hurt animation
++    /**
++     * Broadcasts a hurt animation. This fakes incoming damage towards the target entity.
++     * <p>
++     * The target players cannot include {@code this} player. For self-damage, use
++     * {@link Player#sendHurtAnimation(float)}.
++     *
++     * @param players the players to broadcast to (cannot include {@code this}
++     * @throws IllegalArgumentException if {@code this} is contained in {@code players}
++     */
++    void broadcastHurtAnimation(@NotNull java.util.Collection<Player> players);
++    // Paper end - broadcast hurt animation
+ }
+diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/entity/Player.java
++++ b/src/main/java/org/bukkit/entity/Player.java
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+     @Override
+     Spigot spigot();
+     // Spigot end
++
++    // Paper start - entity effect API
++    /**
++     * Plays an entity effect to this player for the target entity
++     * <p>
++     * If the effect is not applicable to this class of entity, it will not play.
++     *
++     * @param effect the entity effect
++     * @param target the target entity
++     */
++    void sendEntityEffect(org.bukkit.@NotNull EntityEffect effect, @NotNull Entity target);
++    // Paper end - entity effect API
+ }
diff --git a/patches/server/Bandaid-fix-for-Effect.patch b/patches/server/Bandaid-fix-for-Effect.patch
index fdb135ad8a..cb883e82e7 100644
--- a/patches/server/Bandaid-fix-for-Effect.patch
+++ b/patches/server/Bandaid-fix-for-Effect.patch
@@ -134,7 +134,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return !Effect.class.getDeclaredField(effect.name()).isAnnotationPresent(Deprecated.class);
 +    }
 +
-+    @SuppressWarnings("deprecation")
 +    @Test
 +    public void checkAllApiExists() throws ReflectiveOperationException {
 +        Map<Integer, Effect> toId = new HashMap<>();
diff --git a/patches/server/Improve-entity-effect-API.patch b/patches/server/Improve-entity-effect-API.patch
new file mode 100644
index 0000000000..155e7e1a4c
--- /dev/null
+++ b/patches/server/Improve-entity-effect-API.patch
@@ -0,0 +1,152 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Fri, 27 Sep 2024 17:13:16 -0700
+Subject: [PATCH] Improve entity effect API
+
+
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+@@ -0,0 +0,0 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+         return this.getHandle().getScoreboardName();
+     }
+     // Paper end - entity scoreboard name
++
++    // Paper start - broadcast hurt animation
++    @Override
++    public void broadcastHurtAnimation(java.util.Collection<Player> players) {
++        //noinspection SuspiciousMethodCalls
++        Preconditions.checkArgument(!players.contains(this), "Cannot broadcast hurt animation to self without a yaw");
++        for (final org.bukkit.entity.Player player : players) {
++            ((CraftPlayer) player).sendHurtAnimation(0, this);
++        }
++    }
++    // Paper end - broadcast hurt animation
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+ 
+     @Override
+     public void sendHurtAnimation(float yaw) {
++        // Paper start - Add target entity to sendHurtAnimation
++        this.sendHurtAnimation(yaw, this);
++    }
++    public void sendHurtAnimation(float yaw, org.bukkit.entity.Entity target) {
++        // Paper end - Add target entity to sendHurtAnimation
+         if (this.getHandle().connection == null) {
+             return;
+         }
+@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+          * This makes no sense. We'll add 90 to it so that 0 = front, clockwise from there.
+          */
+         float actualYaw = yaw + 90;
+-        this.getHandle().connection.send(new ClientboundHurtAnimationPacket(this.getEntityId(), actualYaw));
++        this.getHandle().connection.send(new ClientboundHurtAnimationPacket(target.getEntityId(), actualYaw)); // Paper - Add target entity to sendHurtAnimation
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+         ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)this.getHandle())
+             .moonrise$getViewDistanceHolder().setSendViewDistance(viewDistance);
+     }
++
++    // Paper start - entity effect API
++    @Override
++    public void sendEntityEffect(final org.bukkit.EntityEffect effect, final org.bukkit.entity.Entity target) {
++        if (this.getHandle().connection == null || !effect.isApplicableTo(target)) {
++            return;
++        }
++        this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundEntityEventPacket(((CraftEntity) target).getHandle(), effect.getData()));
++    }
++    // Paper end - entity effect API
+ }
+diff --git a/src/test/java/org/bukkit/EntityEffectTest.java b/src/test/java/org/bukkit/EntityEffectTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/org/bukkit/EntityEffectTest.java
+@@ -0,0 +0,0 @@
++package org.bukkit;
++
++import com.google.common.base.Joiner;
++import java.lang.reflect.Field;
++import java.lang.reflect.Modifier;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import net.minecraft.world.entity.EntityEvent;
++import org.junit.jupiter.api.Test;
++
++import static org.junit.jupiter.api.Assertions.fail;
++
++public class EntityEffectTest {
++
++    private static List<Byte> collectNmsLevelEvents() throws ReflectiveOperationException {
++        final List<Byte> events = new ArrayList<>();
++        for (final Field field : EntityEvent.class.getFields()) {
++            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()) && field.getType() == byte.class) {
++                events.add((byte) field.get(null));
++            }
++        }
++        for (int i = 22; i <= 28; i++) {
++            events.remove(Byte.valueOf((byte) i)); // all have existing API (debug info and op level)
++        }
++        events.remove(Byte.valueOf(EntityEvent.STOP_ATTACKING)); // not used on client anywhere
++        events.remove(Byte.valueOf(EntityEvent.USE_ITEM_COMPLETE)); // not suitable for API (complete using item on Player)
++        events.remove(Byte.valueOf(EntityEvent.FISHING_ROD_REEL_IN)); // not suitable for API (fishing rod reel in on FishingHook)
++        events.add((byte) 0); // handled on Arrow (for some reason it's not in the EntityEvent nms file as a constant)
++        return events;
++    }
++
++    private static boolean isNotDeprecated(EntityEffect effect) throws ReflectiveOperationException {
++        return !EntityEffect.class.getDeclaredField(effect.name()).isAnnotationPresent(Deprecated.class);
++    }
++
++    @Test
++    public void checkAllApiExists() throws ReflectiveOperationException {
++        Map<Byte, EntityEffect> toId = new HashMap<>();
++        for (final EntityEffect effect : EntityEffect.values()) {
++            if (isNotDeprecated(effect)) {
++                toId.put(effect.getData(), effect);
++            }
++        }
++
++        final Set<Byte> missingEvents = new HashSet<>();
++        for (final Byte event : collectNmsLevelEvents()) {
++            if (toId.get(event) == null) {
++                missingEvents.add(event);
++            }
++        }
++        if (!missingEvents.isEmpty()) {
++            fail("Missing API EntityEffects:\n" + Joiner.on("\n").join(missingEvents));
++        }
++    }
++
++    @Test
++    public void checkNoExtraApi() throws ReflectiveOperationException {
++        Map<Byte, EntityEffect> toId = new HashMap<>();
++        for (final EntityEffect effect : EntityEffect.values()) {
++            if (isNotDeprecated(effect)) {
++                toId.put(effect.getData(), effect);
++            }
++        }
++
++        final List<Byte> nmsEvents = collectNmsLevelEvents();
++        final Set<EntityEffect> extraApiEffects = new HashSet<>();
++        for (final Map.Entry<Byte, EntityEffect> entry : toId.entrySet()) {
++            if (!nmsEvents.contains(entry.getKey())) {
++                extraApiEffects.add(entry.getValue());
++            }
++        }
++        if (!extraApiEffects.isEmpty()) {
++            fail("Extra API EntityEffects:\n" + Joiner.on("\n").join(extraApiEffects));
++        }
++    }
++}