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());
+ }
+}