Restore paintings and item frames on Folia without teleporting (#1011)

* Restore paintings and item frames on Folia without teleporting

Rollback spawns a painting or item frame and then teleports it into place, but Folia always throws from Entity#teleport, so paintings are lost and frames never get their recorded facing or item. On Folia the entity is now spawned directly at its recorded block with its facing, art or item set in the pre-spawn consumer. Paper and Spigot are unchanged.

* Spawn Folia hangings through the adapter on 1.19.4 and 1.20.1

Compiling against the current API binds World#spawn(Location, Class,
Consumer) to the java.util.function.Consumer overload, which only exists
from Bukkit 1.20.2. Folia 1.19.4 and 1.20.1 only have the
org.bukkit.util.Consumer overload, so restoring a painting or item frame
there threw NoSuchMethodError. The spawn now goes through
BukkitAdapter.spawn, which calls the java.util.function.Consumer overload
when World has it and otherwise invokes the org.bukkit.util.Consumer
overload by reflection.
This commit is contained in:
Sage Kummer authored and GitHub committed 2026-10-02 13:32:46 -06:00
1 parent 4274f88d0a
commit 80caab5938
4 files changed
+99

No files matched your search

@@ -1,6 +1,8 @@
package net.coreprotect.bukkit;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@@ -8,6 +10,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.bukkit.Art;
import org.bukkit.Chunk;
@@ -395,6 +398,43 @@ public class BukkitAdapter implements BukkitInterface {
return ItemFrame.class;
}
@Override
public <T extends Entity> T spawn(World world, Location location, Class<T> entityClass, Consumer<? super T> function) {
// Bukkit 1.20.1 and earlier only have World#spawn(Location, Class, org.bukkit.util.Consumer), which is deprecated for removal in the current API
try {
Class<?> consumerClass = Class.forName("org.bukkit.util.Consumer");
Object consumer = Proxy.newProxyInstance(consumerClass.getClassLoader(), new Class<?>[] { consumerClass }, (proxy, method, args) -> {
if (method.getDeclaringClass() == Object.class) {
switch (method.getName()) {
case "equals":
return proxy == args[0];
case "hashCode":
return System.identityHashCode(proxy);
default:
return consumerClass.getName();
}
}
function.accept(entityClass.cast(args[0]));
return null;
});
Method spawnMethod = World.class.getMethod("spawn", Location.class, Class.class, consumerClass);
return entityClass.cast(spawnMethod.invoke(world, location, entityClass, consumer));
}
catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new IllegalStateException(cause);
}
catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}
@Override
public boolean isDecoratedPot(Material material) {
return false;
@@ -3,6 +3,7 @@ package net.coreprotect.bukkit;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.bukkit.Art;
import org.bukkit.Chunk;
@@ -377,6 +378,21 @@ public interface BukkitInterface {
*/
Class<?> getFrameClass(Material material);
/**
* Spawns an entity, running a function on it before it is added to the world.
*
* @param world
* The world
* @param location
* The spawn location
* @param entityClass
* The entity class
* @param function
* The function to run before the entity is added
* @return The spawned entity
*/
<T extends Entity> T spawn(World world, Location location, Class<T> entityClass, Consumer<? super T> function);
// --------------------------------------------------------------------------
// Sign methods
// --------------------------------------------------------------------------
@@ -4,16 +4,20 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.bukkit.Color;
import org.bukkit.DyeColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.BlockState;
import org.bukkit.block.ChiseledBookshelf;
import org.bukkit.block.Sign;
import org.bukkit.block.sign.Side;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
@@ -40,6 +44,7 @@ public class Bukkit_v1_20 extends Bukkit_v1_19 {
private Boolean hasClickedPosition;
private Boolean hasBasePotionType;
private Boolean hasItemName;
private Boolean hasSpawnFunction;
/**
* Initializes the Bukkit_v1_20 adapter with 1.20-specific block groups and mappings.
@@ -281,6 +286,22 @@ public class Bukkit_v1_20 extends Bukkit_v1_19 {
return hasItemName && itemMeta.hasItemName() ? itemMeta.getItemName() : "";
}
@Override
public <T extends Entity> T spawn(World world, Location location, Class<T> entityClass, Consumer<? super T> function) {
if (hasSpawnFunction == null) {
try {
// Check if World#spawn accepts java.util.function.Consumer (Bukkit 1.20.2+)
World.class.getMethod("spawn", Location.class, Class.class, Consumer.class);
hasSpawnFunction = true;
}
catch (NoSuchMethodException e) {
hasSpawnFunction = false;
}
}
return hasSpawnFunction ? world.spawn(location, entityClass, function) : super.spawn(world, location, entityClass, function);
}
@Override
public ItemStack getArrowMeta(Arrow arrow, ItemStack itemStack) {
try {
@@ -15,6 +15,7 @@ import org.bukkit.entity.Painting;
import org.bukkit.inventory.ItemStack;
import net.coreprotect.bukkit.BukkitAdapter;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.MaterialUtils;
@@ -131,6 +132,15 @@ public class HangingUtil {
if (hangingFace == null) {
BlockUtils.setTypeAndData(spawnBlock, Material.AIR, null, true);
}
if (ConfigHandler.isFolia) {
// Folia does not support Entity#teleport, so the painting is placed at its target before it is added to the world
BlockFace facing = faceSet;
BukkitAdapter.ADAPTER.spawn(block.getWorld(), block.getWorld().getBlockAt(paintingX, paintingY, paintingZ).getLocation(), Painting.class, spawned -> {
spawned.setFacingDirection(facing, true);
spawned.setArt(painting, true);
});
return;
}
Painting hanging = null;
try {
hanging = block.getWorld().spawn(spawnBlock.getLocation(), Painting.class);
@@ -150,6 +160,18 @@ public class HangingUtil {
BlockUtils.setTypeAndData(spawnBlock, Material.AIR, null, true);
}
Class itemFrame = BukkitAdapter.ADAPTER.getFrameClass(rowType);
if (ConfigHandler.isFolia) {
// Folia does not support Entity#teleport, so the frame is placed at its target before it is added to the world
BlockFace facing = faceSet;
Material frameItem = MaterialUtils.getType(rowData);
BukkitAdapter.ADAPTER.spawn(block.getWorld(), block.getWorld().getBlockAt(x, y, z).getLocation(), (Class<? extends ItemFrame>) itemFrame, spawned -> {
spawned.setFacingDirection(facing, true);
if (frameItem != null) {
spawned.setItem(new ItemStack(frameItem, 1));
}
});
return;
}
Entity entity = block.getWorld().spawn(spawnBlock.getLocation(), itemFrame);
if (entity instanceof ItemFrame) {
ItemFrame hanging = (ItemFrame) entity;