diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/CreeperSwellEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/CreeperSwellEvent.java new file mode 100644 index 000000000000..a38038418d63 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/entity/CreeperSwellEvent.java @@ -0,0 +1,161 @@ +package io.papermc.paper.event.entity; + +import org.bukkit.entity.Creeper; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.entity.EntityEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +/** + * Fired whenever a creeper's swell level changes. + *

+ * If this event is cancelled, the swell change will not be applied, but the client may still render the swell change. + */ +@NullMarked +public class CreeperSwellEvent extends EntityEvent implements Cancellable { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private boolean cancelled; + private int swellChange; + private int finalSwellChange; + private final int currentSwell; + private final int maxSwell; + private final SwellReason swellReason; + + @ApiStatus.Internal + public CreeperSwellEvent(final Creeper creeper, int swellChange, int finalSwellChange, int currentSwell, + int maxSwell, SwellReason swellReason) { + super(creeper); + this.swellChange = swellChange; + this.finalSwellChange = finalSwellChange; + this.currentSwell = currentSwell; + this.maxSwell = maxSwell; + this.swellReason = swellReason; + } + + /** + * Gets the cancellation state of this event. A cancelled event will not + * be executed in the server, but will still pass to other plugins + * + * @return {@code true} if this event is cancelled + */ + @Override + public boolean isCancelled() { + return this.cancelled; + } + + /** + * Sets the cancellation state of this event. A cancelled event will not + * be executed in the server, but will still pass to other plugins. + * + * @param cancel {@code true} if you wish to cancel this event + */ + @Override + public void setCancelled(final boolean cancel) { + this.cancelled = cancel; + } + + /** + * Returns the Entity involved in this event + * + * @return Entity who is involved in this event + */ + @Override + public Creeper getEntity() { + return (Creeper) super.getEntity(); + } + + /** + * Gets the current swell level of the {@link Creeper} + * + * @return current swell level of the {@link Creeper} + */ + public int getCurrentSwell() { + return this.currentSwell; + } + + /** + * Gets the current maximum swell level of the {@link Creeper} + * + * @return maximum swell level of the {@link Creeper} + */ + public int getMaxSwell() { + return this.maxSwell; + } + + /** + * Gets the raw swell change before clamping + * + * @return the raw swell change + */ + public int getSwellChange() { + return this.swellChange; + } + + /** + * Gets the final swell change that will actually be applied to the {@link Creeper} + * + * @return the final swell change + */ + public int getFinalSwellChange() { + return this.finalSwellChange; + } + + /** + * Sets the swell change to apply to the {@link Creeper} + *

+ * The swell change will be clamped to keep the {@link Creeper}'s swell level within [0, maxSwell] + * + * @param newSwellChange the swell change to apply + * @see #getFinalSwellChange() + */ + public void setSwellChange(int newSwellChange) { + this.swellChange = newSwellChange; + // don't allow creeper swell level to go below 0 or above maxSwell + if (this.currentSwell + newSwellChange < 0) { + this.finalSwellChange = -this.currentSwell; + } else if (this.currentSwell + newSwellChange > this.maxSwell) { + this.finalSwellChange = this.maxSwell - this.currentSwell; + } else { + this.finalSwellChange = newSwellChange; + } + } + + /** + * Gets the reason for the swell change + * + * @return a SwellReason value detailing the cause of the swell change + */ + public SwellReason getSwellReason() { + return this.swellReason; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + /** + * An enum to specify the reason for a creeper swell change + */ + public enum SwellReason { + /** + * Swelling caused by the creeper being primed (by proximity or ignition) + */ + PRIMED, + /** + * Swelling caused by the creeper falling from a height + */ + FALL_DAMAGE, + /** + * Swelling caused by custom behavior, such as a plugin + */ + CUSTOM + } +} diff --git a/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch index 7f4bb9c08612..a7a02a210f9c 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/monster/Creeper.java.patch @@ -8,6 +8,30 @@ public Creeper(final EntityType type, final Level level) { super(type, level); +@@ -86,10 +_,20 @@ + @Override + public boolean causeFallDamage(final double fallDistance, final float damageModifier, final DamageSource damageSource) { + boolean damaged = super.causeFallDamage(fallDistance, damageModifier, damageSource); +- this.swell += (int)(fallDistance * 1.5); +- if (this.swell > this.maxSwell - 5) { +- this.swell = this.maxSwell - 5; ++ // Paper start - CreeperSwellEvent; fire event for fall damage swell ++ int swellChange = (int)(fallDistance * 1.5); ++ int maxAllowedSwell = this.maxSwell - 5; ++ int finalSwellChange = Math.min(swellChange, maxAllowedSwell - this.swell); ++ if (finalSwellChange > 0) { ++ io.papermc.paper.event.entity.CreeperSwellEvent event = new io.papermc.paper.event.entity.CreeperSwellEvent( ++ (org.bukkit.entity.Creeper) this.getBukkitEntity(), swellChange, finalSwellChange, this.swell, ++ this.maxSwell, io.papermc.paper.event.entity.CreeperSwellEvent.SwellReason.FALL_DAMAGE); ++ event.callEvent(); ++ if (!event.isCancelled()) { ++ this.swell += event.getFinalSwellChange(); ++ } + } ++ // Paper end - CreeperSwellEvent + + return damaged; + } @@ -118,7 +_,7 @@ this.maxSwell = input.getShortOr("Fuse", (short)30); this.explosionRadius = input.getByteOr("ExplosionRadius", (byte)3); @@ -17,6 +41,35 @@ } } +@@ -131,12 +_,23 @@ + } + + int swellDir = this.getSwellDir(); +- if (swellDir > 0 && this.swell == 0) { +- this.playSound(SoundEvents.CREEPER_PRIMED, 1.0F, 0.5F); +- this.gameEvent(GameEvent.PRIME_FUSE); ++ // Paper start - CreeperSwellEvent; fire if swell will change this tick ++ if (swellDir != 0) { ++ int finalSwellChange = Math.clamp(swellDir, -this.swell, this.maxSwell - this.swell); ++ io.papermc.paper.event.entity.CreeperSwellEvent event = new io.papermc.paper.event.entity.CreeperSwellEvent( ++ (org.bukkit.entity.Creeper) this.getBukkitEntity(), swellDir, finalSwellChange, this.swell, this.maxSwell, ++ io.papermc.paper.event.entity.CreeperSwellEvent.SwellReason.PRIMED); ++ event.callEvent(); ++ if (!event.isCancelled()) { ++ int change = event.getFinalSwellChange(); ++ if (change > 0 && this.swell == 0) { ++ this.playSound(SoundEvents.CREEPER_PRIMED, 1.0F, 0.5F); ++ this.gameEvent(GameEvent.PRIME_FUSE); ++ } ++ this.swell += change; ++ } + } +- +- this.swell += swellDir; ++ // Paper end - CreeperSwellEvent + if (this.swell < 0) { + this.swell = 0; + } @@ -151,10 +_,11 @@ } @@ -63,7 +116,7 @@ itemStack.shrink(1); } else { itemStack.hurtAndBreak(1, player, hand.asEquipmentSlot()); -@@ -231,18 +_,29 @@ +@@ -231,18 +_,38 @@ public void explodeCreeper() { if (this.level() instanceof ServerLevel level) { float explosionMultiplier = this.isPowered() ? 2.0F : 1.0F; @@ -80,7 +133,16 @@ + this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause + // CraftBukkit start + } else { -+ this.swell = 0; ++ // Paper start - CreeperSwellEvent; reset swell through event when explosion is cancelled ++ io.papermc.paper.event.entity.CreeperSwellEvent swellEvent = ++ new io.papermc.paper.event.entity.CreeperSwellEvent((org.bukkit.entity.Creeper) getBukkitEntity(), ++ -this.swell, -this.swell, this.swell, this.maxSwell, ++ io.papermc.paper.event.entity.CreeperSwellEvent.SwellReason.CUSTOM); ++ swellEvent.callEvent(); ++ if (!swellEvent.isCancelled()) { ++ this.swell += swellEvent.getFinalSwellChange(); ++ } ++ // Paper end - CreeperSwellEvent + this.entityData.set(DATA_IS_IGNITED, false); // Paper + } + // CraftBukkit end