diff --git a/paper-api/src/main/java/org/bukkit/entity/Entity.java b/paper-api/src/main/java/org/bukkit/entity/Entity.java
index dc91a3cf3b..e99cacc078 100644
--- a/paper-api/src/main/java/org/bukkit/entity/Entity.java
+++ b/paper-api/src/main/java/org/bukkit/entity/Entity.java
@@ -5,6 +5,7 @@ import java.util.Set;
import java.util.UUID;
import io.papermc.paper.datacomponent.DataComponentView;
import io.papermc.paper.entity.LookAnchor;
+import net.kyori.adventure.util.TriState;
import org.bukkit.Chunk; // Paper
import org.bukkit.EntityEffect;
import org.bukkit.Location;
@@ -296,17 +297,43 @@ public interface Entity extends Metadatable, CommandSender, Nameable, Persistent
/**
* Sets if the entity has visual fire (it will always appear to be on fire).
*
+ * @deprecated This method doesn't allow visually extinguishing a burning entity,
+ * use {@link #setVisualFire(TriState)} instead
* @param fire whether visual fire is enabled
*/
+ @Deprecated
void setVisualFire(boolean fire);
+ /**
+ * Sets if the entity has visual fire (it will always appear to be on fire).
+ *
+ * - {@link TriState#NOT_SET} – will revert the entity's visual fire to default
+ * - {@link TriState#TRUE} – will make the entity appear to be on fire
+ * - {@link TriState#FALSE} – will make the entity appear to be not on fire
+ *
+ *
+ * @param fire a TriState value representing the state of the visual fire.
+ */
+ void setVisualFire(@NotNull TriState fire);
+
/**
* Gets if the entity has visual fire (it will always appear to be on fire).
*
+ * @deprecated This method can't properly reflect the three possible states of visual fire,
+ * use {@link #getVisualFire()} instead
* @return whether visual fire is enabled
*/
+ @Deprecated
boolean isVisualFire();
+ /**
+ * Retrieves the visual fire state of the object.
+ *
+ * @return A TriState indicating the current visual fire state.
+ */
+ @NotNull
+ TriState getVisualFire();
+
/**
* Returns the entity's current freeze ticks (amount of ticks the entity has
* been in powdered snow).
diff --git a/paper-server/patches/features/0015-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0015-Moonrise-optimisation-patches.patch
index 29e206088b..23184702aa 100644
--- a/paper-server/patches/features/0015-Moonrise-optimisation-patches.patch
+++ b/paper-server/patches/features/0015-Moonrise-optimisation-patches.patch
@@ -28597,7 +28597,7 @@ index 8cc5c0716392ba06501542ff5cbe71ee43979e5d..09fd99c9cbd23b5f3c899bfb00c9b896
+ // Paper end - block counting
}
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
-index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080df422be7 100644
+index 663fb13233afb51f935c30ac2acae808809754c6..81a18b8e605bd4c28b48a32c80be231609182970 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -140,7 +140,7 @@ import net.minecraft.world.scores.ScoreHolder;
@@ -28950,7 +28950,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
}
private static float[] collectCandidateStepUpHeights(AABB box, List colliders, float deltaY, float maxUpStep) {
-@@ -2600,21 +2748,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -2616,21 +2764,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public boolean isInWall() {
@@ -29072,7 +29072,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
}
public InteractionResult interact(Player player, InteractionHand hand) {
-@@ -4062,15 +4299,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4078,15 +4315,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public Iterable getIndirectPassengers() {
@@ -29098,7 +29098,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
}
public int countPlayerPassengers() {
-@@ -4213,77 +4452,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4229,77 +4468,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return Mth.lerp(partialTick, this.yRotO, this.yRot);
}
@@ -29289,7 +29289,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
public boolean touchingUnloadedChunk() {
AABB aabb = this.getBoundingBox().inflate(1.0);
-@@ -4438,6 +4736,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4454,6 +4752,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) {
@@ -29305,7 +29305,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
if (!checkPosition(this, x, y, z)) {
return;
}
-@@ -4571,6 +4878,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4587,6 +4894,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public final void setRemoved(Entity.RemovalReason removalReason, @Nullable org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { // CraftBukkit - add Bukkit remove cause
@@ -29318,7 +29318,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
if (this.removalReason == null) {
-@@ -4581,7 +4894,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4597,7 +4910,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.stopRiding();
}
@@ -29327,7 +29327,7 @@ index be8213fa58e8305976c5ce16c9ff32130a26d42c..ace6c77be333e839b679b5cf3cd7c080
this.levelCallback.onRemove(removalReason);
this.onRemoval(removalReason);
// Paper start - Folia schedulers
-@@ -4615,7 +4928,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4631,7 +4944,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
public boolean shouldBeSaved() {
return (this.removalReason == null || this.removalReason.shouldSave())
&& !this.isPassenger()
diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
index 428679de20..10e24def5c 100644
--- a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
+++ b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch
@@ -126,6 +126,15 @@
private final double[] pistonDeltas = new double[]{0.0, 0.0, 0.0};
private long pistonDeltasGameTime;
private EntityDimensions dimensions;
+@@ -251,7 +_,7 @@
+ private boolean onGroundNoBlocks = false;
+ private float crystalSoundIntensity;
+ private int lastCrystalSoundPlayTick;
+- public boolean hasVisualFire;
++ public net.kyori.adventure.util.TriState visualFire = net.kyori.adventure.util.TriState.NOT_SET; // Paper - improve visual fire API
+ @Nullable
+ private BlockState inBlockState = null;
+ private final List> movementThisTick = new ObjectArrayList<>();
@@ -259,6 +_,41 @@
private final LongSet visitedBlocks = new LongOpenHashSet();
private final InsideBlockEffectApplier.StepBasedCollector insideEffectCollector = new InsideBlockEffectApplier.StepBasedCollector();
@@ -392,7 +401,12 @@
}
this.checkBelowWorld();
-@@ -504,7 +_,12 @@
+@@ -500,11 +_,16 @@
+ }
+
+ public void setSharedFlagOnFire(boolean isOnFire) {
+- this.setSharedFlag(0, isOnFire || this.hasVisualFire);
++ this.setSharedFlag(0, this.visualFire.toBooleanOrElse(isOnFire)); // Paper - improve visual fire API
}
public void checkBelowWorld() {
@@ -761,6 +775,21 @@
Component customName = this.getCustomName();
if (customName != null) {
RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE);
+@@ -1848,9 +_,12 @@
+ compound.putInt("TicksFrozen", this.getTicksFrozen());
+ }
+
+- if (this.hasVisualFire) {
+- compound.putBoolean("HasVisualFire", this.hasVisualFire);
++ // Paper start - improve visual fire API
++ if (this.visualFire.equals(net.kyori.adventure.util.TriState.TRUE)) {
++ compound.putBoolean("HasVisualFire", true);
+ }
++ compound.putString("Paper.FireOverride", visualFire.name());
++ // Paper end
+
+ if (!this.tags.isEmpty()) {
+ compound.store("Tags", TAG_LIST_CODEC, List.copyOf(this.tags));
@@ -1860,13 +_,13 @@
compound.store("data", CustomData.CODEC, this.customData);
}
@@ -820,6 +849,28 @@
Vec2 vec2 = compound.read("Rotation", Vec2.CODEC).orElse(Vec2.ZERO);
this.setDeltaMovement(Math.abs(vec31.x) > 10.0 ? 0.0 : vec31.x, Math.abs(vec31.y) > 10.0 ? 0.0 : vec31.y, Math.abs(vec31.z) > 10.0 ? 0.0 : vec31.z);
this.hasImpulse = true;
+@@ -1921,7 +_,20 @@
+ this.setNoGravity(compound.getBooleanOr("NoGravity", false));
+ this.setGlowingTag(compound.getBooleanOr("Glowing", false));
+ this.setTicksFrozen(compound.getIntOr("TicksFrozen", 0));
+- this.hasVisualFire = compound.getBooleanOr("HasVisualFire", false);
++ // Paper start - improve visual fire API
++ compound.getString("Paper.FireOverride").ifPresentOrElse(
++ override -> {
++ try {
++ this.visualFire = net.kyori.adventure.util.TriState.valueOf(override);
++ } catch (final Exception ignored) {
++ LOGGER.error("Unknown fire override {} for {}", override, this);
++ }
++ },
++ () -> this.visualFire = compound.getBoolean("HasVisualFire")
++ .map(net.kyori.adventure.util.TriState::byBoolean)
++ .orElse(net.kyori.adventure.util.TriState.NOT_SET)
++ );
++ // Paper end
+ this.customData = compound.read("data", CustomData.CODEC).orElse(CustomData.EMPTY);
+ this.tags.clear();
+ compound.read("Tags", TAG_LIST_CODEC).ifPresent(this.tags::addAll);
@@ -1932,6 +_,67 @@
} else {
throw new IllegalStateException("Entity has invalid rotation");
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
index cd145edc45..cfc926c97c 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
@@ -13,6 +13,7 @@ import java.util.Set;
import java.util.UUID;
import io.papermc.paper.entity.LookAnchor;
import java.util.concurrent.CompletableFuture;
+import net.kyori.adventure.util.TriState;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
@@ -395,13 +396,25 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
}
@Override
+ @Deprecated
public void setVisualFire(boolean fire) {
- this.getHandle().hasVisualFire = fire;
+ setVisualFire(fire ? TriState.TRUE : TriState.NOT_SET);
+ }
+
+ @Override
+ public void setVisualFire(final TriState fire) {
+ Preconditions.checkArgument(fire != null, "TriState cannot be null");
+ this.getHandle().visualFire = fire;
}
@Override
public boolean isVisualFire() {
- return this.getHandle().hasVisualFire;
+ return getVisualFire().toBooleanOrElse(false);
+ }
+
+ @Override
+ public TriState getVisualFire() {
+ return this.getHandle().visualFire;
}
@Override