From da0950329f66dcdf06dd6a2c8c7226a51e53873b Mon Sep 17 00:00:00 2001 From: DeidaraMC <117625071+DeidaraMC@users.noreply.github.com> Date: Mon, 27 May 2024 11:59:48 -0400 Subject: [PATCH] feat: LivingEntity fire overhaul (#2122) * feat: LivingEntity fire overhaul * chore: missed method change --- .../net/minestom/server/entity/Entity.java | 12 -- .../minestom/server/entity/LivingEntity.java | 100 +++++--------- .../net/minestom/server/entity/Player.java | 4 +- .../server/event/entity/EntityFireEvent.java | 53 -------- .../entity/EntityFireExtinguishEvent.java | 38 ++++++ .../event/entity/EntitySetFireEvent.java | 42 ++++++ .../server/entity/EntityFireTest.java | 125 ++++++++++++++++++ 7 files changed, 239 insertions(+), 135 deletions(-) delete mode 100644 src/main/java/net/minestom/server/event/entity/EntityFireEvent.java create mode 100644 src/main/java/net/minestom/server/event/entity/EntityFireExtinguishEvent.java create mode 100644 src/main/java/net/minestom/server/event/entity/EntitySetFireEvent.java create mode 100644 src/test/java/net/minestom/server/entity/EntityFireTest.java diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 6315af522..c42cbc796 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -1062,18 +1062,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev return this.entityMeta.isOnFire(); } - /** - * Sets the entity in fire visually. - *

- * WARNING: if you want to apply damage or specify a duration, - * see {@link LivingEntity#setFireForDuration(int, TemporalUnit)}. - * - * @param fire should the entity be set in fire - */ - public void setOnFire(boolean fire) { - this.entityMeta.setOnFire(fire); - } - /** * Gets if the entity is sneaking. *

diff --git a/src/main/java/net/minestom/server/entity/LivingEntity.java b/src/main/java/net/minestom/server/entity/LivingEntity.java index c6f5fc531..efac09af6 100644 --- a/src/main/java/net/minestom/server/entity/LivingEntity.java +++ b/src/main/java/net/minestom/server/entity/LivingEntity.java @@ -12,7 +12,8 @@ import net.minestom.server.entity.metadata.LivingEntityMeta; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.entity.EntityDamageEvent; import net.minestom.server.event.entity.EntityDeathEvent; -import net.minestom.server.event.entity.EntityFireEvent; +import net.minestom.server.event.entity.EntityFireExtinguishEvent; +import net.minestom.server.event.entity.EntitySetFireEvent; import net.minestom.server.event.item.EntityEquipEvent; import net.minestom.server.event.item.PickupItemEvent; import net.minestom.server.instance.EntityTracker; @@ -61,14 +62,9 @@ public class LivingEntity extends Entity implements EquipmentHandler { protected boolean invulnerable; /** - * Time at which this entity must be extinguished + * Ticks until this entity must be extinguished */ - private long fireExtinguishTime; - - /** - * Period, in ms, between two fire damage applications - */ - private long fireDamagePeriod = 1000L; + private int remainingFireTicks; private Team team; @@ -186,8 +182,9 @@ public class LivingEntity extends Entity implements EquipmentHandler { @Override public void update(long time) { - if (isOnFire() && time > fireExtinguishTime) { - setOnFire(false); + // Fire + if (remainingFireTicks > 0 && --remainingFireTicks == 0) { + EventDispatcher.callCancellable(new EntityFireExtinguishEvent(this, true), () -> entityMeta.setOnFire(false)); } // Items picking @@ -272,44 +269,40 @@ public class LivingEntity extends Entity implements EquipmentHandler { } /** - * Sets fire to this entity for a given duration. + * Gets the amount of ticks this entity is on fire for. * - * @param duration duration in ticks of the effect + * @return the remaining duration of fire in ticks, 0 if not on fire */ - public void setFireForDuration(int duration) { - setFireForDuration(duration, TimeUnit.SERVER_TICK); + public int getFireTicks() { + return remainingFireTicks; } /** - * Sets fire to this entity for a given duration. + * Sets this entity on fire for the given ticks. * - * @param duration duration of the effect - * @param temporalUnit unit used to express the duration - * @see #setOnFire(boolean) if you want it to be permanent without any event callback + * @param ticks duration of fire in ticks */ - public void setFireForDuration(int duration, TemporalUnit temporalUnit) { - setFireForDuration(Duration.of(duration, temporalUnit)); - } + public void setFireTicks(int ticks) { + int fireTicks = Math.max(0, ticks); + if (fireTicks > 0) { + EntitySetFireEvent entitySetFireEvent = new EntitySetFireEvent(this, ticks); + EventDispatcher.call(entitySetFireEvent); + if (entitySetFireEvent.isCancelled()) return; - /** - * Sets fire to this entity for a given duration. - * - * @param duration duration of the effect - * @see #setOnFire(boolean) if you want it to be permanent without any event callback - */ - public void setFireForDuration(Duration duration) { - EntityFireEvent entityFireEvent = new EntityFireEvent(this, duration); - - // Do not start fire event if the fire needs to be removed (< 0 duration) - if (duration.toMillis() > 0) { - EventDispatcher.callCancellable(entityFireEvent, () -> { - final long fireTime = entityFireEvent.getFireTime(TimeUnit.MILLISECOND); - setOnFire(true); - fireExtinguishTime = System.currentTimeMillis() + fireTime; - }); - } else { - fireExtinguishTime = System.currentTimeMillis(); + fireTicks = Math.max(0, entitySetFireEvent.getFireTicks()); + if (fireTicks > 0) { + remainingFireTicks = fireTicks; + entityMeta.setOnFire(true); + return; + } } + + if (remainingFireTicks != 0) { + EntityFireExtinguishEvent entityFireExtinguishEvent = new EntityFireExtinguishEvent(this, false); + EventDispatcher.callCancellable(entityFireExtinguishEvent, () -> entityMeta.setOnFire(false)); + } + + remainingFireTicks = fireTicks; } public boolean damage(@NotNull DynamicRegistry.Key type, float amount) { @@ -568,35 +561,6 @@ public class LivingEntity extends Entity implements EquipmentHandler { return new EntityAttributesPacket(getEntityId(), List.copyOf(attributeModifiers.values())); } - /** - * Gets the time in ms between two fire damage applications. - * - * @return the time in ms - * @see #setFireDamagePeriod(Duration) - */ - public long getFireDamagePeriod() { - return fireDamagePeriod; - } - - /** - * Changes the delay between two fire damage applications. - * - * @param fireDamagePeriod the delay - * @param temporalUnit the time unit - */ - public void setFireDamagePeriod(long fireDamagePeriod, @NotNull TemporalUnit temporalUnit) { - setFireDamagePeriod(Duration.of(fireDamagePeriod, temporalUnit)); - } - - /** - * Changes the delay between two fire damage applications. - * - * @param fireDamagePeriod the delay - */ - public void setFireDamagePeriod(Duration fireDamagePeriod) { - this.fireDamagePeriod = fireDamagePeriod.toMillis(); - } - /** * Changes the {@link Team} for the entity. * diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 84d74ff1a..dc7cd2cc8 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -519,8 +519,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable, if (!isDead()) return; - setFireForDuration(0); - setOnFire(false); + setFireTicks(0); + entityMeta.setOnFire(false); refreshHealth(); sendPacket(new RespawnPacket(DIMENSION_TYPE_REGISTRY.getId(getDimensionType().namespace()), instance.getDimensionName(), diff --git a/src/main/java/net/minestom/server/event/entity/EntityFireEvent.java b/src/main/java/net/minestom/server/event/entity/EntityFireEvent.java deleted file mode 100644 index a7b4e301e..000000000 --- a/src/main/java/net/minestom/server/event/entity/EntityFireEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.minestom.server.event.entity; - -import net.minestom.server.entity.Entity; -import net.minestom.server.event.trait.CancellableEvent; -import net.minestom.server.event.trait.EntityInstanceEvent; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.time.temporal.TemporalUnit; - -public class EntityFireEvent implements EntityInstanceEvent, CancellableEvent { - - private final Entity entity; - private Duration duration; - - private boolean cancelled; - - public EntityFireEvent(Entity entity, int duration, TemporalUnit temporalUnit) { - this(entity, Duration.of(duration, temporalUnit)); - } - - public EntityFireEvent(Entity entity, Duration duration) { - this.entity = entity; - setFireTime(duration); - } - - public long getFireTime(TemporalUnit temporalUnit) { - return duration.toNanos() / temporalUnit.getDuration().toNanos(); - } - - public void setFireTime(int duration, TemporalUnit temporalUnit) { - setFireTime(Duration.of(duration, temporalUnit)); - } - - public void setFireTime(Duration duration) { - this.duration = duration; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public void setCancelled(boolean cancel) { - this.cancelled = cancel; - } - - @Override - public @NotNull Entity getEntity() { - return entity; - } -} diff --git a/src/main/java/net/minestom/server/event/entity/EntityFireExtinguishEvent.java b/src/main/java/net/minestom/server/event/entity/EntityFireExtinguishEvent.java new file mode 100644 index 000000000..e24cc8602 --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/EntityFireExtinguishEvent.java @@ -0,0 +1,38 @@ +package net.minestom.server.event.entity; + +import net.minestom.server.entity.Entity; +import net.minestom.server.event.trait.CancellableEvent; +import net.minestom.server.event.trait.EntityInstanceEvent; +import org.jetbrains.annotations.NotNull; + +public class EntityFireExtinguishEvent implements EntityInstanceEvent, CancellableEvent { + + private final Entity entity; + private boolean natural; + + private boolean cancelled; + + public EntityFireExtinguishEvent(Entity entity, boolean natural) { + this.entity = entity; + this.natural = natural; + } + + public boolean isNatural() { + return natural; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull Entity getEntity() { + return entity; + } +} diff --git a/src/main/java/net/minestom/server/event/entity/EntitySetFireEvent.java b/src/main/java/net/minestom/server/event/entity/EntitySetFireEvent.java new file mode 100644 index 000000000..e82db94a5 --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/EntitySetFireEvent.java @@ -0,0 +1,42 @@ +package net.minestom.server.event.entity; + +import net.minestom.server.entity.Entity; +import net.minestom.server.event.trait.CancellableEvent; +import net.minestom.server.event.trait.EntityInstanceEvent; +import org.jetbrains.annotations.NotNull; + +public class EntitySetFireEvent implements EntityInstanceEvent, CancellableEvent { + + private final Entity entity; + private int ticks; + + private boolean cancelled; + + public EntitySetFireEvent(Entity entity, int ticks) { + this.entity = entity; + this.ticks = ticks; + } + + public int getFireTicks() { + return ticks; + } + + public void setFireTicks(int ticks) { + this.ticks = ticks; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull Entity getEntity() { + return entity; + } +} diff --git a/src/test/java/net/minestom/server/entity/EntityFireTest.java b/src/test/java/net/minestom/server/entity/EntityFireTest.java new file mode 100644 index 000000000..a9c45431e --- /dev/null +++ b/src/test/java/net/minestom/server/entity/EntityFireTest.java @@ -0,0 +1,125 @@ +package net.minestom.server.entity; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.event.entity.EntityFireExtinguishEvent; +import net.minestom.server.event.entity.EntitySetFireEvent; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +@EnvTest +public class EntityFireTest +{ + @Test + public void duration(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + + final int fireTicks = 10; + LivingEntity entity = new LivingEntity(EntityType.ZOMBIE); + entity.setInstance(instance, new Vec(0, 0, 0)); + + entity.setFireTicks(fireTicks); + assertTrue(entity.getEntityMeta().isOnFire()); + + for (int i = 0; i < fireTicks; i++) { + assertTrue(entity.getEntityMeta().isOnFire()); + assertEquals(fireTicks - i, entity.getFireTicks()); + entity.tick(0); + } + + assertFalse(entity.getEntityMeta().isOnFire()); + assertEquals(entity.getFireTicks(), 0); + } + + @Test + public void nonNegativeFireDuration(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + + LivingEntity entity = new LivingEntity(EntityType.ZOMBIE); + entity.setInstance(instance, new Vec(0, 0, 0)); + + // Natural fire decay + entity.setFireTicks(5); + for (int i = 0; i < 20; i++) { + assertTrue(entity.getFireTicks() >= 0); + } + + // Explicit negative + entity.setFireTicks(-1); + assertEquals(0, entity.getFireTicks()); + + // Explicit negative in event + env.listen(EntitySetFireEvent.class).followup(e -> { + e.setFireTicks(-1); + }); + + entity.setFireTicks(1); + assertEquals(entity.getFireTicks(), 0); + } + + @Test + public void setFireMetadata(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + + LivingEntity entity = new LivingEntity(EntityType.ZOMBIE); + entity.setInstance(instance, new Vec(0, 0, 0)); + + // Do not extinguish an entity when they're set on fire explicitly + entity.getEntityMeta().setOnFire(true); + for (int i = 0; i < 40; i++) { + entity.tick(0); + assertTrue(entity.getEntityMeta().isOnFire()); + } + + // Unless setFireTicks has been called to activate the internal remainingFireTicks timer + entity.setFireTicks(1); + entity.tick(0); + assertFalse(entity.isOnFire()); + } + + @Test + public void extinguishEvent(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + + LivingEntity entity = new LivingEntity(EntityType.ZOMBIE); + entity.setInstance(instance, new Vec(0, 0, 0)); + + AtomicInteger callCount = new AtomicInteger(); + env.listen(EntityFireExtinguishEvent.class).followup(e -> { + callCount.getAndIncrement(); + if (callCount.get() == 2) assertTrue(e.isNatural()); + else assertFalse(e.isNatural()); + }); + + // Don't call when the entity is already on fire + entity.setFireTicks(0); + assertEquals(0, callCount.get()); + + // Call now, the entity is set on fire + entity.setFireTicks(1); + entity.setFireTicks(-1); + assertEquals(1, callCount.get()); + + // Call naturally + entity.setFireTicks(3); + for (int i = 0; i < 3; i++) { + entity.tick(0); + } + assertEquals(2, callCount.get()); + + // Don't call if cancelled EntitySetFireEvent + env.listen(EntitySetFireEvent.class).followup(e -> { + e.setCancelled(true); + }); + entity.setFireTicks(5); + assertEquals(2, callCount.get()); + } +}