Improved Folia support (fixes #892)

This commit is contained in:
Intelli committed 2026-04-23 13:45:30 -06:00
1 parent f5ac148b57
commit 4d311f5248
11 files changed
+191 -88

No files matched your search

@@ -14,7 +14,6 @@ import net.coreprotect.utility.Chat;
import net.coreprotect.utility.ChatMessage; import net.coreprotect.utility.ChatMessage;
import net.coreprotect.utility.Color; import net.coreprotect.utility.Color;
import net.coreprotect.utility.Teleport; import net.coreprotect.utility.Teleport;
import net.coreprotect.utility.Util;
import net.coreprotect.utility.WorldUtils; import net.coreprotect.utility.WorldUtils;
public class TeleportCommand { public class TeleportCommand {
@@ -56,6 +55,10 @@ public class TeleportCommand {
World world = location.getWorld(); World world = location.getWorld();
if (wid > -1) { if (wid > -1) {
world = Bukkit.getServer().getWorld(WorldUtils.getWorldName(wid)); world = Bukkit.getServer().getWorld(WorldUtils.getWorldName(wid));
if (world == null) {
Chat.sendMessage(player, new ChatMessage(Phrase.build(Phrase.WORLD_NOT_FOUND, worldName)).build());
return;
}
} }
String x = null; String x = null;
@@ -10,16 +10,20 @@ import org.bukkit.entity.EntityType;
import net.coreprotect.consumer.Consumer; import net.coreprotect.consumer.Consumer;
import net.coreprotect.database.logger.EntityKillLogger; import net.coreprotect.database.logger.EntityKillLogger;
import net.coreprotect.utility.EntityUtils; import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.Util;
class EntityKillProcess { class EntityKillProcess {
static void process(PreparedStatement preparedStmt, PreparedStatement preparedStmtEntities, int batchCount, int processId, int id, Object object, String user) { static void process(PreparedStatement preparedStmt, PreparedStatement preparedStmtEntities, int batchCount, int processId, int id, Object object, String user) {
if (object instanceof Object[]) { if (object instanceof Object[]) {
BlockState block = (BlockState) ((Object[]) object)[0]; Object[] values = (Object[]) object;
EntityType type = (EntityType) ((Object[]) object)[1]; if (values.length <= 1 || !(values[0] instanceof BlockState) || !(values[1] instanceof EntityType)) {
return;
}
BlockState block = (BlockState) values[0];
EntityType type = (EntityType) values[1];
Map<Integer, List<Object>> objectLists = Consumer.consumerObjectList.get(processId); Map<Integer, List<Object>> objectLists = Consumer.consumerObjectList.get(processId);
if (objectLists.get(id) != null) { if (objectLists != null && objectLists.get(id) != null) {
List<Object> objectList = objectLists.get(id); List<Object> objectList = objectLists.get(id);
int entityId = EntityUtils.getEntityId(type); int entityId = EntityUtils.getEntityId(type);
EntityKillLogger.log(preparedStmt, preparedStmtEntities, batchCount, user, block, objectList, entityId); EntityKillLogger.log(preparedStmt, preparedStmtEntities, batchCount, user, block, objectList, entityId);
@@ -10,8 +10,13 @@ class PlayerKillProcess {
static void process(PreparedStatement preparedStmt, int batchCount, int id, Object object, String user) { static void process(PreparedStatement preparedStmt, int batchCount, int id, Object object, String user) {
if (object instanceof Object[]) { if (object instanceof Object[]) {
BlockState block = (BlockState) ((Object[]) object)[0]; Object[] values = (Object[]) object;
String player = (String) ((Object[]) object)[1]; if (values.length <= 1 || !(values[0] instanceof BlockState) || !(values[1] instanceof String)) {
return;
}
BlockState block = (BlockState) values[0];
String player = (String) values[1];
PlayerKillLogger.log(preparedStmt, batchCount, user, block, player); PlayerKillLogger.log(preparedStmt, batchCount, user, block, player);
} }
} }
@@ -10,6 +10,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
@@ -247,19 +248,21 @@ public class Rollback extends RollbackUtil {
} }
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount, blockCount, entityCount, 0, scannedWorldData }); ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount, blockCount, entityCount, 0, scannedWorldData });
List<CompletableFuture<Boolean>> chunkFutures = new ArrayList<>();
for (Entry<Integer, World> rollbackWorlds : worldMap.entrySet()) { for (Entry<Integer, World> rollbackWorlds : worldMap.entrySet()) {
Integer rollbackWorldId = rollbackWorlds.getKey(); Integer rollbackWorldId = rollbackWorlds.getKey();
World bukkitRollbackWorld = rollbackWorlds.getValue(); World bukkitRollbackWorld = rollbackWorlds.getValue();
Location chunkLocation = new Location(bukkitRollbackWorld, (finalChunkX << 4), 0, (finalChunkZ << 4));
final HashMap<Long, ArrayList<Object[]>> finalBlockList = dataList.get(rollbackWorldId); final HashMap<Long, ArrayList<Object[]>> finalBlockList = dataList.get(rollbackWorldId);
final HashMap<Long, ArrayList<Object[]>> finalItemList = itemDataList.get(rollbackWorldId); final HashMap<Long, ArrayList<Object[]>> finalItemList = itemDataList.get(rollbackWorldId);
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> { chunkFutures.add(scheduleChunkTask(finalChunkX, finalChunkZ, chunkKey, finalBlockList, finalItemList, rollbackType, preview, finalUserString, finalUser, bukkitRollbackWorld, inventoryRollback));
// Process this chunk using our new RollbackProcessor class }
ArrayList<Object[]> blockData = finalBlockList != null ? finalBlockList.getOrDefault(chunkKey, new ArrayList<>()) : new ArrayList<>();
ArrayList<Object[]> itemData = finalItemList != null ? finalItemList.getOrDefault(chunkKey, new ArrayList<>()) : new ArrayList<>(); if (ConfigHandler.isFolia) {
RollbackProcessor.processChunk(finalChunkX, finalChunkZ, chunkKey, blockData, itemData, rollbackType, preview, finalUserString, finalUser instanceof Player ? (Player) finalUser : null, bukkitRollbackWorld, inventoryRollback); if (!awaitChunkTasks(chunkFutures, preview)) {
}, chunkLocation, 0); Chat.console(Phrase.build(Phrase.ROLLBACK_ABORTED));
break;
}
} }
rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString); rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString);
@@ -268,24 +271,26 @@ public class Rollback extends RollbackUtil {
int sleepTime = 0; int sleepTime = 0;
int abort = 0; int abort = 0;
while (next == 0 || scannedWorlds < worldMap.size()) { if (!ConfigHandler.isFolia) {
if (preview == 1) { while (next == 0 || scannedWorlds < worldMap.size()) {
// Not actually changing blocks, so less intensive. if (preview == 1) {
sleepTime = sleepTime + 1; // Not actually changing blocks, so less intensive.
Thread.sleep(1); sleepTime = sleepTime + 1;
} Thread.sleep(1);
else { }
sleepTime = sleepTime + 5; else {
Thread.sleep(5); sleepTime = sleepTime + 5;
} Thread.sleep(5);
}
rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString); rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString);
next = rollbackHashData[3]; next = rollbackHashData[3];
scannedWorlds = rollbackHashData[4]; scannedWorlds = rollbackHashData[4];
if (sleepTime > 300000) { if (sleepTime > 300000) {
abort = 1; abort = 1;
break; break;
}
} }
} }
@@ -331,4 +336,69 @@ public class Rollback extends RollbackUtil {
return null; return null;
} }
private static CompletableFuture<Boolean> scheduleChunkTask(int chunkX, int chunkZ, long chunkKey, HashMap<Long, ArrayList<Object[]>> blockList, HashMap<Long, ArrayList<Object[]>> itemList, int rollbackType, int preview, String userString, CommandSender user, World world, boolean inventoryRollback) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
Location chunkLocation = new Location(world, (chunkX << 4), 0, (chunkZ << 4));
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> {
try {
ArrayList<Object[]> blockData = blockList != null ? blockList.getOrDefault(chunkKey, new ArrayList<>()) : new ArrayList<>();
ArrayList<Object[]> itemData = itemList != null ? itemList.getOrDefault(chunkKey, new ArrayList<>()) : new ArrayList<>();
Player rollbackPlayer = user instanceof Player ? (Player) user : null;
boolean result = RollbackProcessor.processChunk(chunkX, chunkZ, chunkKey, blockData, itemData, rollbackType, preview, userString, rollbackPlayer, world, inventoryRollback);
future.complete(result);
}
catch (Exception e) {
e.printStackTrace();
future.complete(false);
}
}, chunkLocation, 0);
return future;
}
private static boolean awaitChunkTasks(List<CompletableFuture<Boolean>> futures, int preview) throws InterruptedException {
if (futures.isEmpty()) {
return true;
}
int sleepTime = 0;
while (true) {
boolean allDone = true;
for (CompletableFuture<Boolean> future : futures) {
if (!future.isDone()) {
allDone = false;
break;
}
}
if (allDone) {
break;
}
int delay = preview == 1 ? 1 : 5;
sleepTime += delay;
if (sleepTime > 300000) {
return false;
}
Thread.sleep(delay);
}
for (CompletableFuture<Boolean> future : futures) {
Boolean result;
try {
result = future.getNow(Boolean.FALSE);
}
catch (Exception e) {
return false;
}
if (!Boolean.TRUE.equals(result)) {
return false;
}
}
return true;
}
} }
@@ -1,5 +1,7 @@
package net.coreprotect.database.rollback; package net.coreprotect.database.rollback;
import java.util.Arrays;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
@@ -78,16 +80,8 @@ public class RollbackEntityHandler {
else if (rowTypeRaw <= 0) { else if (rowTypeRaw <= 0) {
// Attempt to remove entity // Attempt to remove entity
if (rowRolledBack == 1) { if (rowRolledBack == 1) {
boolean removed = false; int entityId = getCachedEntityId(rowX, rowY, rowZ, rowWorldId, oldTypeRaw);
int entityId = -1; EntityType targetType = EntityUtils.getEntityType(oldTypeRaw);
String entityName = EntityUtils.getEntityType(oldTypeRaw).name();
String token = "" + rowX + "." + rowY + "." + rowZ + "." + rowWorldId + "." + entityName + "";
Object[] cachedEntity = CacheHandler.entityCache.get(token);
if (cachedEntity != null) {
entityId = (Integer) cachedEntity[1];
}
int xmin = rowX - 5; int xmin = rowX - 5;
int xmax = rowX + 5; int xmax = rowX + 5;
int ymin = rowY - 1; int ymin = rowY - 1;
@@ -95,43 +89,10 @@ public class RollbackEntityHandler {
int zmin = rowZ - 5; int zmin = rowZ - 5;
int zmax = rowZ + 5; int zmax = rowZ + 5;
for (Entity entity : block.getChunk().getEntities()) { boolean removed = removeMatchingEntity(Arrays.asList(block.getChunk().getEntities()), finalUserString, entityId, targetType, xmin, xmax, ymin, ymax, zmin, zmax, true);
if (entityId > -1) { if (!removed && entityId > -1 && !ConfigHandler.isFolia) {
int id = entity.getEntityId(); // On non-Folia, keep world-wide fallback for moved entities.
if (id == entityId) { removed = removeMatchingEntity(block.getWorld().getLivingEntities(), finalUserString, entityId, targetType, xmin, xmax, ymin, ymax, zmin, zmax, false);
updateEntityCount(finalUserString, 1);
removed = true;
entity.remove();
break;
}
}
else {
if (entity.getType().equals(EntityUtils.getEntityType(oldTypeRaw))) {
Location entityLocation = entity.getLocation();
int entityx = entityLocation.getBlockX();
int entityY = entityLocation.getBlockY();
int entityZ = entityLocation.getBlockZ();
if (entityx >= xmin && entityx <= xmax && entityY >= ymin && entityY <= ymax && entityZ >= zmin && entityZ <= zmax) {
updateEntityCount(finalUserString, 1);
removed = true;
entity.remove();
break;
}
}
}
}
if (!removed && entityId > -1) {
for (Entity entity : block.getWorld().getLivingEntities()) {
int id = entity.getEntityId();
if (id == entityId) {
updateEntityCount(finalUserString, 1);
removed = true;
entity.remove();
break;
}
}
} }
if (removed) { if (removed) {
@@ -159,6 +120,48 @@ public class RollbackEntityHandler {
return WorldUtils.getWorldName(worldId); return WorldUtils.getWorldName(worldId);
} }
private static int getCachedEntityId(int rowX, int rowY, int rowZ, int rowWorldId, int oldTypeRaw) {
String entityName = EntityUtils.getEntityType(oldTypeRaw).name();
String token = rowX + "." + rowY + "." + rowZ + "." + rowWorldId + "." + entityName;
Object[] cachedEntity = CacheHandler.entityCache.get(token);
if (cachedEntity != null && cachedEntity.length > 1 && cachedEntity[1] instanceof Integer) {
return (Integer) cachedEntity[1];
}
return -1;
}
private static boolean removeMatchingEntity(Iterable<? extends Entity> entities, String userString, int cachedEntityId, EntityType targetType, int xmin, int xmax, int ymin, int ymax, int zmin, int zmax, boolean useBounds) {
for (Entity entity : entities) {
if (cachedEntityId > -1) {
if (entity.getEntityId() == cachedEntityId) {
updateEntityCount(userString, 1);
entity.remove();
return true;
}
continue;
}
if (!entity.getType().equals(targetType)) {
continue;
}
if (useBounds && !isWithinBounds(entity.getLocation(), xmin, xmax, ymin, ymax, zmin, zmax)) {
continue;
}
updateEntityCount(userString, 1);
entity.remove();
return true;
}
return false;
}
private static boolean isWithinBounds(Location location, int xmin, int xmax, int ymin, int ymax, int zmin, int zmax) {
int entityX = location.getBlockX();
int entityY = location.getBlockY();
int entityZ = location.getBlockZ();
return entityX >= xmin && entityX <= xmax && entityY >= ymin && entityY <= ymax && entityZ >= zmin && entityZ <= zmax;
}
/** /**
* Updates the entity count in the rollback hash for a specific user. * Updates the entity count in the rollback hash for a specific user.
* *
@@ -25,11 +25,13 @@ import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import net.coreprotect.CoreProtect;
import net.coreprotect.bukkit.BukkitAdapter; import net.coreprotect.bukkit.BukkitAdapter;
import net.coreprotect.config.Config; import net.coreprotect.config.Config;
import net.coreprotect.config.ConfigHandler; import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.logger.ItemLogger; import net.coreprotect.database.logger.ItemLogger;
import net.coreprotect.model.BlockGroup; import net.coreprotect.model.BlockGroup;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.utility.BlockUtils; import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.ItemUtils; import net.coreprotect.utility.ItemUtils;
import net.coreprotect.utility.MaterialUtils; import net.coreprotect.utility.MaterialUtils;
@@ -435,7 +437,7 @@ public class RollbackProcessor {
int chunkZ = playerLocation.getBlockZ() >> 4; int chunkZ = playerLocation.getBlockZ() >> 4;
if (bukkitRollbackWorld.getName().equals(playerWorld) && chunkX == finalChunkX && chunkZ == finalChunkZ) { if (bukkitRollbackWorld.getName().equals(playerWorld) && chunkX == finalChunkX && chunkZ == finalChunkZ) {
Teleport.performSafeTeleport(player, playerLocation, false); Scheduler.runTask(CoreProtect.getInstance(), () -> Teleport.performSafeTeleport(player, playerLocation, false), player);
} }
} }
} }
@@ -84,7 +84,10 @@ public final class BlockDispenseListener extends Queue implements Listener {
} }
if (!dispenseSuccess && material == Material.BONE_MEAL) { if (!dispenseSuccess && material == Material.BONE_MEAL) {
CacheHandler.redstoneCache.put(newBlock.getLocation(), new Object[] { System.currentTimeMillis(), user }); String key = CacheHandler.locationKey(newBlock.getLocation());
if (!key.isEmpty()) {
CacheHandler.redstoneCache.put(key, new Object[] { System.currentTimeMillis(), user });
}
} }
if (type == Material.FIRE && (!config.BLOCK_IGNITE || !(newBlockData instanceof Lightable))) { if (type == Material.FIRE && (!config.BLOCK_IGNITE || !(newBlockData instanceof Lightable))) {
@@ -59,7 +59,8 @@ public final class BlockFertilizeListener extends Queue implements Listener {
user = player.getName(); user = player.getName();
} }
else { else {
Object[] data = CacheHandler.redstoneCache.get(location); String key = CacheHandler.locationKey(location);
Object[] data = CacheHandler.redstoneCache.get(key);
if (data != null) { if (data != null) {
long newTime = System.currentTimeMillis(); long newTime = System.currentTimeMillis();
long oldTime = (long) data[0]; long oldTime = (long) data[0];
@@ -67,7 +68,7 @@ public final class BlockFertilizeListener extends Queue implements Listener {
user = (String) data[1]; user = (String) data[1];
} }
CacheHandler.redstoneCache.remove(location); CacheHandler.redstoneCache.remove(key);
} }
} }
@@ -23,7 +23,14 @@ public class CacheHandler implements Runnable {
public static ConcurrentHashMap<String, Object[]> flowDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2); public static ConcurrentHashMap<String, Object[]> flowDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static ConcurrentHashMap<String, Object[]> bonemealDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2); public static ConcurrentHashMap<String, Object[]> bonemealDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static ConcurrentHashMap<String, Object[]> entityKillDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2); public static ConcurrentHashMap<String, Object[]> entityKillDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static ConcurrentHashMap<Location, Object[]> redstoneCache = new ConcurrentHashMap<>(16, 0.75f, 2); public static ConcurrentHashMap<String, Object[]> redstoneCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static String locationKey(Location location) {
if (location == null || location.getWorld() == null) {
return "";
}
return location.getWorld().getUID() + ":" + location.getBlockX() + ":" + location.getBlockY() + ":" + location.getBlockZ();
}
public static boolean shouldSuppressRepeat(ConcurrentHashMap<String, Object[]> cache, String cacheId, int threshold, int resetWindowSeconds) { public static boolean shouldSuppressRepeat(ConcurrentHashMap<String, Object[]> cache, String cacheId, int threshold, int resetWindowSeconds) {
if (cache == null || cacheId == null || cacheId.isEmpty() || threshold <= 0) { if (cache == null || cacheId == null || cacheId.isEmpty() || threshold <= 0) {
@@ -29,10 +29,10 @@ public class Scheduler {
else if (regionData instanceof Entity) { else if (regionData instanceof Entity) {
Entity entity = (Entity) regionData; Entity entity = (Entity) regionData;
if (delay == 0) { if (delay == 0) {
entity.getScheduler().run(plugin, value -> task.run(), task); entity.getScheduler().run(plugin, value -> task.run(), null);
} }
else { else {
entity.getScheduler().runDelayed(plugin, value -> task.run(), task, delay); entity.getScheduler().runDelayed(plugin, value -> task.run(), null, delay);
} }
} }
else { else {
@@ -62,7 +62,7 @@ public class Scheduler {
} }
else if (regionData instanceof Entity) { else if (regionData instanceof Entity) {
Entity entity = (Entity) regionData; Entity entity = (Entity) regionData;
return entity.getScheduler().runAtFixedRate(plugin, value -> task.run(), task, delay, period); return entity.getScheduler().runAtFixedRate(plugin, value -> task.run(), null, delay, period);
} }
else { else {
return plugin.getServer().getGlobalRegionScheduler().runAtFixedRate(plugin, value -> task.run(), delay, period); return plugin.getServer().getGlobalRegionScheduler().runAtFixedRate(plugin, value -> task.run(), delay, period);
@@ -74,12 +74,17 @@ public class EntityUtil {
if (type == null) { if (type == null) {
return; return;
} }
final Location blockLocation = block.getLocation();
if (blockLocation.getWorld() == null) {
return;
}
Scheduler.runTask(CoreProtect.getInstance(), () -> { Scheduler.runTask(CoreProtect.getInstance(), () -> {
try { try {
Location location = block.getLocation(); Location location = blockLocation.clone();
location.setX(location.getX() + 0.50); location.setX(location.getX() + 0.50);
location.setZ(location.getZ() + 0.50); location.setZ(location.getZ() + 0.50);
Entity entity = block.getLocation().getWorld().spawnEntity(location, type); Entity entity = blockLocation.getWorld().spawnEntity(location, type);
if (list.isEmpty()) { if (list.isEmpty()) {
return; return;
@@ -599,7 +604,7 @@ public class EntityUtil {
catch (Exception e) { catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
}, block.getLocation()); }, blockLocation);
} }
} }