Optimize rollback chunk batching and prefetching

This commit is contained in:
Intelli committed 2026-07-05 15:31:17 -06:00
1 parent a0ae242dac
commit 9d04c025be
13 files changed
+384 -257

No files matched your search

@@ -1,8 +1,11 @@
package net.coreprotect.consumer.process;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import net.coreprotect.consumer.Consumer;
import net.coreprotect.database.Database;
@@ -15,13 +18,19 @@ class RollbackUpdateProcess {
Map<Integer, List<Object[]>> updateLists = Consumer.consumerObjectArrayList.get(processId);
if (updateLists.get(id) != null) {
List<Object[]> list = updateLists.get(id);
boolean inventoryState = RollbackUpdateTargets.usesInventoryRollbackState(table);
Map<Integer, List<Long>> rowIdsByValue = new HashMap<>();
for (Object[] listRow : list) {
long rowid = (Long) listRow[0];
int rolledBack = (Integer) listRow[9];
if (MaterialUtils.rolledBack(rolledBack, RollbackUpdateTargets.usesInventoryRollbackState(table)) == action) { // 1 = restore, 0 = rollback
Database.performUpdate(statement, rowid, rolledBack, table);
if (MaterialUtils.rolledBack(rolledBack, inventoryState) == action) { // 1 = restore, 0 = rollback
int toggledValue = MaterialUtils.toggleRolledBack(rolledBack, inventoryState);
rowIdsByValue.computeIfAbsent(toggledValue, key -> new ArrayList<>()).add(rowid);
}
}
for (Entry<Integer, List<Long>> entry : rowIdsByValue.entrySet()) {
Database.performRolledBackUpdate(statement, entry.getKey(), entry.getValue(), table);
}
updateLists.remove(id);
}
}
@@ -30,7 +30,6 @@ import net.coreprotect.utility.Chat;
import net.coreprotect.utility.Color;
import net.coreprotect.utility.HopperTransactionUtils;
import net.coreprotect.utility.ItemUtils;
import net.coreprotect.utility.MaterialUtils;
import net.coreprotect.utility.ErrorReporter;
public class Database extends Queue {
@@ -50,6 +49,8 @@ public class Database extends Queue {
public static final int BLOCKDATA = 12;
public static final int ITEM = 13;
private static final int ROLLED_BACK_UPDATE_BATCH_SIZE = 1000;
private static final Map<Integer, String> SQL_QUERIES = new HashMap<>();
static {
@@ -224,17 +225,31 @@ public class Database extends Queue {
}
}
public static void performUpdate(Statement statement, long id, int rb, int table) {
public static void performRolledBackUpdate(Statement statement, int rolledBack, List<Long> rowIds, int table) {
String tableName;
if (RollbackUpdateTargets.updatesContainerTable(table)) {
tableName = "container";
}
else if (RollbackUpdateTargets.updatesItemTable(table)) {
tableName = "item";
}
else {
tableName = "block";
}
try {
int rolledBack = MaterialUtils.toggleRolledBack(rb, RollbackUpdateTargets.usesInventoryRollbackState(table));
if (RollbackUpdateTargets.updatesContainerTable(table)) {
statement.executeUpdate("UPDATE " + ConfigHandler.prefix + "container SET rolled_back='" + rolledBack + "' WHERE rowid='" + id + "'");
}
else if (RollbackUpdateTargets.updatesItemTable(table)) {
statement.executeUpdate("UPDATE " + ConfigHandler.prefix + "item SET rolled_back='" + rolledBack + "' WHERE rowid='" + id + "'");
}
else {
statement.executeUpdate("UPDATE " + ConfigHandler.prefix + "block SET rolled_back='" + rolledBack + "' WHERE rowid='" + id + "'");
int listSize = rowIds.size();
for (int startIndex = 0; startIndex < listSize; startIndex += ROLLED_BACK_UPDATE_BATCH_SIZE) {
int endIndex = Math.min(startIndex + ROLLED_BACK_UPDATE_BATCH_SIZE, listSize);
StringBuilder query = new StringBuilder("UPDATE " + ConfigHandler.prefix + tableName + " SET rolled_back='" + rolledBack + "' WHERE rowid IN(");
for (int index = startIndex; index < endIndex; index++) {
if (index > startIndex) {
query.append(",");
}
query.append(rowIds.get(index).longValue());
}
query.append(")");
statement.executeUpdate(query.toString());
}
}
catch (Exception e) {
@@ -20,7 +20,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import net.coreprotect.CoreProtect;
import net.coreprotect.bukkit.BukkitAdapter;
import net.coreprotect.config.Config;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.consumer.Queue;
@@ -33,7 +32,9 @@ import net.coreprotect.language.Selector;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.model.action.LookupActions;
import net.coreprotect.model.rollback.RollbackUpdateTargets;
import net.coreprotect.paper.PaperAdapter;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.thread.TickTimeMonitor;
import net.coreprotect.utility.Chat;
import net.coreprotect.utility.Color;
import net.coreprotect.utility.DatabaseUtils;
@@ -42,7 +43,11 @@ import net.coreprotect.utility.ErrorReporter;
public class Rollback extends RollbackUtil {
private static final long ROLLBACK_BATCH_BUDGET_NANOS = 25_000_000L;
private static final long ROLLBACK_BATCH_BUDGET_BASELINE_NANOS = 25_000_000L;
private static final long ROLLBACK_BATCH_BUDGET_FLOOR_NANOS = 20_000_000L;
private static final long ROLLBACK_BATCH_BUDGET_CEILING_NANOS = 50_000_000L;
private static final long ROLLBACK_BATCH_TICK_YIELD_NANOS = 50_000_000L;
private static final int CHUNK_PREFETCH_DISTANCE = 8;
public static List<String[]> performRollbackRestore(Statement statement, CommandSender user, List<String> checkUuids, List<String> checkUsers, String timeString, List<Object> restrictList, Map<Object, Boolean> excludeList, List<String> excludeUserList, List<Integer> actionList, Location location, Integer[] radius, long startTime, long endTime, boolean restrictWorld, boolean lookup, boolean verbose, final int rollbackType, final int preview) {
List<String[]> list = new ArrayList<>();
@@ -96,10 +101,6 @@ public class Rollback extends RollbackUtil {
HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList = new HashMap<>();
boolean inventoryRollback = actionList.contains(LookupActions.ITEM);
int worldId = -1;
int worldMin = 0;
int worldMax = 2032;
int listC = 0;
while (listC < 2) {
List<Object[]> scanList = lookupList;
@@ -118,15 +119,6 @@ public class Rollback extends RollbackUtil {
int chunkZ = rowZ >> 4;
long chunkKey = inventoryRollback ? 0 : (chunkX & 0xffffffffL | (chunkZ & 0xffffffffL) << 32);
if (rowWorldId != worldId) {
String world = WorldUtils.getWorldName(rowWorldId);
World bukkitWorld = Bukkit.getServer().getWorld(world);
if (bukkitWorld != null) {
worldMin = BukkitAdapter.ADAPTER.getMinHeight(bukkitWorld);
worldMax = bukkitWorld.getMaxHeight();
}
}
if (chunkList.get(chunkKey) == null) {
int distance = 0;
if (location != null) {
@@ -219,12 +211,13 @@ public class Rollback extends RollbackUtil {
ConfigHandler.rollbackHash.put(userString, new int[] { 0, 0, 0, 0, 0 });
final String finalUserString = userString;
RollbackBlockDataCache blockDataCache = new RollbackBlockDataCache();
List<Entry<Long, Integer>> sortedChunks = new ArrayList<>(DatabaseUtils.entriesSortedByValues(chunkList));
if (ConfigHandler.isFolia) {
chunkCount = processFoliaChunks(sortedChunks, worldList, dataList, itemDataList, rollbackType, preview, finalUserString, user, inventoryRollback, verbose, actionList);
chunkCount = processFoliaChunks(sortedChunks, worldList, dataList, itemDataList, rollbackType, preview, finalUserString, user, inventoryRollback, verbose, actionList, blockDataCache);
}
else {
chunkCount = processBukkitChunks(sortedChunks, worldList, dataList, itemDataList, rollbackType, preview, finalUserString, user, inventoryRollback, verbose, actionList);
chunkCount = processBukkitChunks(sortedChunks, worldList, dataList, itemDataList, rollbackType, preview, finalUserString, user, inventoryRollback, verbose, actionList, blockDataCache);
}
chunkList.clear();
@@ -252,7 +245,7 @@ public class Rollback extends RollbackUtil {
return null;
}
private static int processFoliaChunks(List<Entry<Long, Integer>> sortedChunks, LinkedHashSet<Integer> worldList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList) throws InterruptedException {
private static int processFoliaChunks(List<Entry<Long, Integer>> sortedChunks, LinkedHashSet<Integer> worldList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList, RollbackBlockDataCache blockDataCache) throws InterruptedException {
FoliaRollbackBatchState batchState = new FoliaRollbackBatchState(buildFoliaChunkWork(sortedChunks, worldList, dataList, itemDataList), sortedChunks.size());
while (batchState.hasNext()) {
if (!processFoliaEmptyChunks(batchState, userString, verbose, user, preview, actionList)) {
@@ -263,7 +256,7 @@ public class Rollback extends RollbackUtil {
break;
}
CompletableFuture<Boolean> batchFuture = scheduleFoliaChunkBatchTask(batchState, rollbackType, preview, userString, user, inventoryRollback, verbose, actionList);
CompletableFuture<Boolean> batchFuture = scheduleFoliaChunkBatchTask(batchState, rollbackType, preview, userString, user, inventoryRollback, verbose, actionList, blockDataCache);
if (!awaitChunkTasks(Collections.singletonList(batchFuture), preview)) {
Chat.console(Phrase.build(Phrase.ROLLBACK_ABORTED));
break;
@@ -316,7 +309,7 @@ public class Rollback extends RollbackUtil {
return true;
}
private static CompletableFuture<Boolean> scheduleFoliaChunkBatchTask(FoliaRollbackBatchState batchState, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList) {
private static CompletableFuture<Boolean> scheduleFoliaChunkBatchTask(FoliaRollbackBatchState batchState, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList, RollbackBlockDataCache blockDataCache) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
FoliaChunkWork firstWork = batchState.peek();
Location chunkLocation = new Location(firstWork.world, (firstWork.chunkX << 4), 0, (firstWork.chunkZ << 4));
@@ -328,7 +321,7 @@ public class Rollback extends RollbackUtil {
FoliaChunkWork work = batchState.next();
prepareChunkCounters(userString);
if (!processChunkWorld(work.chunkX, work.chunkZ, work.chunkKey, work.blockList, work.itemList, rollbackType, preview, userString, user, work.world, inventoryRollback)) {
if (!processChunkWorld(work.chunkX, work.chunkZ, work.chunkKey, work.blockList, work.itemList, rollbackType, preview, userString, user, work.world, inventoryRollback, blockDataCache)) {
future.complete(false);
return;
}
@@ -354,53 +347,118 @@ public class Rollback extends RollbackUtil {
return future;
}
private static int processBukkitChunks(List<Entry<Long, Integer>> sortedChunks, LinkedHashSet<Integer> worldList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList) throws InterruptedException {
private static int processBukkitChunks(List<Entry<Long, Integer>> sortedChunks, LinkedHashSet<Integer> worldList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList, RollbackBlockDataCache blockDataCache) throws InterruptedException {
HashMap<Integer, World> worldMap = getRollbackWorlds(worldList);
RollbackBatchState batchState = new RollbackBatchState(sortedChunks);
while (batchState.hasNext()) {
CompletableFuture<Boolean> batchFuture = scheduleChunkBatchTask(batchState, worldList, dataList, itemDataList, rollbackType, preview, userString, user, inventoryRollback, verbose, actionList);
if (!awaitChunkTasks(Collections.singletonList(batchFuture), preview)) {
Chat.console(Phrase.build(Phrase.ROLLBACK_ABORTED));
break;
}
if (!batchState.hasNext()) {
return 0;
}
CompletableFuture<Boolean> completion = new CompletableFuture<>();
scheduleChunkBatchTask(batchState, worldMap, dataList, itemDataList, rollbackType, preview, userString, user, inventoryRollback, verbose, actionList, blockDataCache, completion, 0);
if (!awaitRollbackCompletion(completion, batchState, preview)) {
Chat.console(Phrase.build(Phrase.ROLLBACK_ABORTED));
}
return batchState.chunkCount;
}
private static CompletableFuture<Boolean> scheduleChunkBatchTask(RollbackBatchState batchState, LinkedHashSet<Integer> worldList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
private static void scheduleChunkBatchTask(RollbackBatchState batchState, HashMap<Integer, World> worldMap, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, boolean verbose, List<Integer> actionList, RollbackBlockDataCache blockDataCache, CompletableFuture<Boolean> completion, int delay) {
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> {
if (completion.isDone()) {
return;
}
try {
long batchBudget = adaptiveBatchBudgetNanos();
long batchStart = System.nanoTime();
do {
prefetchUpcomingChunks(batchState, worldMap, inventoryRollback);
Entry<Long, Integer> entry = batchState.next();
batchState.chunkCount++;
if (!processChunkEntry(entry, worldList, dataList, itemDataList, rollbackType, preview, userString, user, inventoryRollback)) {
future.complete(false);
if (!processChunkEntry(entry, worldMap, dataList, itemDataList, rollbackType, preview, userString, user, inventoryRollback, blockDataCache)) {
completion.complete(false);
return;
}
if (!completeChunk(userString, batchState.chunkCount, batchState.totalChunks(), verbose, user, preview, actionList)) {
future.complete(false);
completion.complete(false);
return;
}
}
while (batchState.hasNext() && (System.nanoTime() - batchStart) < ROLLBACK_BATCH_BUDGET_NANOS);
while (batchState.hasNext() && (System.nanoTime() - batchStart) < batchBudget);
future.complete(true);
if (batchState.hasNext()) {
batchState.nanosSinceTickYield += (System.nanoTime() - batchStart);
int nextDelay = 0;
if (batchState.nanosSinceTickYield >= ROLLBACK_BATCH_TICK_YIELD_NANOS) {
batchState.nanosSinceTickYield = 0;
nextDelay = 1;
}
scheduleChunkBatchTask(batchState, worldMap, dataList, itemDataList, rollbackType, preview, userString, user, inventoryRollback, verbose, actionList, blockDataCache, completion, nextDelay);
}
else {
completion.complete(true);
}
}
catch (Exception e) {
ErrorReporter.report(e);
future.complete(false);
completion.complete(false);
}
}, 0);
}, delay);
}
return future;
private static boolean awaitRollbackCompletion(CompletableFuture<Boolean> completion, RollbackBatchState batchState, int preview) throws InterruptedException {
int delay = preview == 1 ? 1 : 5;
int lastChunkCount = -1;
long stalledTime = 0;
while (!completion.isDone()) {
int chunkCount = batchState.chunkCount;
if (chunkCount != lastChunkCount) {
lastChunkCount = chunkCount;
stalledTime = 0;
}
stalledTime += delay;
if (stalledTime > 300000) {
completion.complete(false);
return false;
}
Thread.sleep(delay);
}
try {
return Boolean.TRUE.equals(completion.getNow(Boolean.FALSE));
}
catch (Exception e) {
return false;
}
}
private static long adaptiveBatchBudgetNanos() {
double averageTickTime = PaperAdapter.ADAPTER.getAverageTickTime(Bukkit.getServer());
if (averageTickTime <= 0.0D) {
averageTickTime = TickTimeMonitor.getEstimatedTickTime();
}
return batchBudgetNanos(averageTickTime);
}
public static long batchBudgetNanos(double averageTickTime) {
if (averageTickTime <= 0.0D) {
return ROLLBACK_BATCH_BUDGET_BASELINE_NANOS;
}
// Scale linearly from the 50ms ceiling on an idle server down to the 20ms floor on an overloaded one, so rollbacks mostly expand into idle tick time
long tickTimeNanos = (long) (averageTickTime * 1_000_000.0D);
long budgetNanos = ROLLBACK_BATCH_BUDGET_BASELINE_NANOS + ROLLBACK_BATCH_BUDGET_CEILING_NANOS - tickTimeNanos;
return Math.max(ROLLBACK_BATCH_BUDGET_FLOOR_NANOS, Math.min(ROLLBACK_BATCH_BUDGET_CEILING_NANOS, budgetNanos));
}
private static boolean canContinueFoliaBatch(FoliaRollbackBatchState batchState, long batchStart) {
if (!batchState.hasNext() || (System.nanoTime() - batchStart) >= ROLLBACK_BATCH_BUDGET_NANOS) {
if (!batchState.hasNext() || (System.nanoTime() - batchStart) >= ROLLBACK_BATCH_BUDGET_BASELINE_NANOS) {
return false;
}
@@ -408,11 +466,10 @@ public class Rollback extends RollbackUtil {
return nextWork.world != null && Bukkit.isOwnedByCurrentRegion(nextWork.world, nextWork.chunkX, nextWork.chunkZ);
}
private static boolean processChunkEntry(Entry<Long, Integer> entry, LinkedHashSet<Integer> worldList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback) {
private static boolean processChunkEntry(Entry<Long, Integer> entry, HashMap<Integer, World> worldMap, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> dataList, HashMap<Integer, HashMap<Long, ArrayList<Object[]>>> itemDataList, int rollbackType, int preview, String userString, CommandSender user, boolean inventoryRollback, RollbackBlockDataCache blockDataCache) {
long chunkKey = entry.getKey();
int chunkX = getChunkX(entry);
int chunkZ = getChunkZ(entry);
HashMap<Integer, World> worldMap = getRollbackWorlds(worldList);
prepareChunkCounters(userString);
for (Entry<Integer, World> rollbackWorlds : worldMap.entrySet()) {
@@ -421,7 +478,7 @@ public class Rollback extends RollbackUtil {
HashMap<Long, ArrayList<Object[]>> blockList = dataList.get(rollbackWorldId);
HashMap<Long, ArrayList<Object[]>> itemList = itemDataList.get(rollbackWorldId);
if (!processChunkWorld(chunkX, chunkZ, chunkKey, blockList, itemList, rollbackType, preview, userString, user, bukkitRollbackWorld, inventoryRollback)) {
if (!processChunkWorld(chunkX, chunkZ, chunkKey, blockList, itemList, rollbackType, preview, userString, user, bukkitRollbackWorld, inventoryRollback, blockDataCache)) {
return false;
}
}
@@ -429,6 +486,23 @@ public class Rollback extends RollbackUtil {
return true;
}
private static void prefetchUpcomingChunks(RollbackBatchState batchState, HashMap<Integer, World> worldMap, boolean inventoryRollback) {
if (inventoryRollback) {
return;
}
int prefetchLimit = Math.min(batchState.index + CHUNK_PREFETCH_DISTANCE, batchState.totalChunks());
while (batchState.prefetchIndex < prefetchLimit) {
Entry<Long, Integer> entry = batchState.sortedChunks.get(batchState.prefetchIndex);
int chunkX = getChunkX(entry);
int chunkZ = getChunkZ(entry);
for (World world : worldMap.values()) {
PaperAdapter.ADAPTER.prefetchChunk(world, chunkX, chunkZ);
}
batchState.prefetchIndex++;
}
}
private static int getChunkX(Entry<Long, Integer> entry) {
return (int) (long) entry.getKey();
}
@@ -486,17 +560,19 @@ public class Rollback extends RollbackUtil {
return true;
}
private static boolean processChunkWorld(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) {
private static boolean processChunkWorld(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, RollbackBlockDataCache blockDataCache) {
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;
return RollbackProcessor.processChunk(chunkX, chunkZ, chunkKey, blockData, itemData, rollbackType, preview, userString, rollbackPlayer, world, inventoryRollback);
return RollbackProcessor.processChunk(chunkX, chunkZ, chunkKey, blockData, itemData, rollbackType, preview, userString, rollbackPlayer, world, inventoryRollback, blockDataCache);
}
private static final class RollbackBatchState {
private final List<Entry<Long, Integer>> sortedChunks;
private int index = 0;
private int chunkCount = 0;
private int prefetchIndex = 0;
private long nanosSinceTickYield = 0L;
private volatile int chunkCount = 0;
private RollbackBatchState(List<Entry<Long, Integer>> sortedChunks) {
this.sortedChunks = sortedChunks;
@@ -0,0 +1,50 @@
package net.coreprotect.database.rollback;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData;
import net.coreprotect.utility.BlockTypeUtils;
import net.coreprotect.utility.BlockUtils;
public final class RollbackBlockDataCache {
private final Map<String, BlockData> parsedBlockData = new HashMap<>();
private final Map<Integer, BlockData> defaultBlockData = new HashMap<>();
BlockData getParsedBlockData(String blockDataString) {
if (blockDataString == null || !blockDataString.contains(":")) {
return null;
}
if (!parsedBlockData.containsKey(blockDataString)) {
BlockData blockData = null;
try {
blockData = BlockTypeUtils.createBlockDataFromString(blockDataString);
if (blockData == null) {
blockData = Bukkit.getServer().createBlockData(blockDataString);
}
}
catch (Exception e) {
// corrupt BlockData, let the server automatically set the BlockData instead
}
parsedBlockData.put(blockDataString, blockData);
}
return cloneBlockData(parsedBlockData.get(blockDataString));
}
BlockData getDefaultBlockData(int rowTypeRaw) {
if (!defaultBlockData.containsKey(rowTypeRaw)) {
defaultBlockData.put(rowTypeRaw, BlockUtils.createBlockData(rowTypeRaw));
}
return cloneBlockData(defaultBlockData.get(rowTypeRaw));
}
private static BlockData cloneBlockData(BlockData blockData) {
return blockData != null ? blockData.clone() : null;
}
}
@@ -40,7 +40,6 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import net.coreprotect.bukkit.BukkitAdapter;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.consumer.Queue;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.model.PendingBlockChange;
@@ -56,7 +55,7 @@ import net.coreprotect.utility.ErrorReporter;
public class RollbackBlockHandler extends Queue {
public static boolean processBlockChange(World bukkitWorld, Block block, Object[] row, int rollbackType, boolean clearInventories, Map<Block, PendingBlockChange> chunkChanges, boolean countBlock, Material oldTypeMaterial, Material pendingChangeType, BlockData pendingChangeData, String finalUserString, BlockData rawBlockData, Material changeType, boolean changeBlock, BlockData changeBlockData, ArrayList<Object> meta, BlockData blockData, String rowUser, Material rowType, int rowX, int rowY, int rowZ, int rowTypeRaw, int rowData, int rowAction, int rowWorldId, String blockDataString) {
public static boolean processBlockChange(World bukkitWorld, Block block, Object[] row, int rollbackType, boolean clearInventories, Map<Block, PendingBlockChange> chunkChanges, boolean countBlock, Material oldTypeMaterial, Material pendingChangeType, BlockData pendingChangeData, RollbackCounters counters, BlockData rawBlockData, Material changeType, boolean changeBlock, BlockData changeBlockData, ArrayList<Object> meta, BlockData blockData, String rowUser, Material rowType, int rowX, int rowY, int rowZ, int rowTypeRaw, int rowData, int rowAction, int rowWorldId, String blockDataString) {
int unixtimestamp = (int) (System.currentTimeMillis() / 1000L);
try {
@@ -234,7 +233,7 @@ public class RollbackBlockHandler extends Queue {
BlockUtils.prepareTypeAndData(chunkChanges, bisectBlock, rowType, null, false);
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
}
}
@@ -281,7 +280,7 @@ public class RollbackBlockHandler extends Queue {
else if (BlockGroup.SHULKER_BOXES.contains(rowType)) {
BlockUtils.prepareTypeAndData(chunkChanges, block, rowType, blockData, false);
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
if (meta != null) {
Inventory inventory = BlockUtils.getContainerInventory(block.getState(), false);
@@ -297,7 +296,7 @@ public class RollbackBlockHandler extends Queue {
else if (rowType == Material.COMMAND_BLOCK || rowType == Material.REPEATING_COMMAND_BLOCK || rowType == Material.CHAIN_COMMAND_BLOCK) { // command block
BlockUtils.prepareTypeAndData(chunkChanges, block, rowType, blockData, false);
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
if (meta != null) {
@@ -333,7 +332,7 @@ public class RollbackBlockHandler extends Queue {
}
else if (blockData == null && rowData > 0 && (rowType == Material.IRON_DOOR || BlockGroup.DOORS.contains(rowType))) {
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
block.setType(rowType, false);
@@ -373,7 +372,7 @@ public class RollbackBlockHandler extends Queue {
}
else if (blockData == null && rowData > 0 && (rowType.name().endsWith("_BED"))) {
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
block.setType(rowType, false);
@@ -404,7 +403,7 @@ public class RollbackBlockHandler extends Queue {
else if (rowType.name().endsWith("_BANNER")) {
BlockUtils.prepareTypeAndData(chunkChanges, block, rowType, blockData, false);
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
if (meta != null) {
@@ -463,7 +462,7 @@ public class RollbackBlockHandler extends Queue {
BlockUtils.prepareTypeAndData(chunkChanges, block, rowType, blockData, false);
if (countBlock) {
updateBlockCount(finalUserString, 2);
counters.addBlocks(2);
}
return false;
}
@@ -475,7 +474,7 @@ public class RollbackBlockHandler extends Queue {
bedData.setPart(Part.HEAD);
BlockUtils.prepareTypeAndData(chunkChanges, adjacentBlock, rowType, bedData, false);
if (countBlock) {
updateBlockCount(finalUserString, 1);
counters.addBlocks(1);
}
}
@@ -549,25 +548,6 @@ public class RollbackBlockHandler extends Queue {
return blockData instanceof MultipleFacing || blockData instanceof Waterlogged || blockData instanceof Bisected || blockData instanceof Bed || blockData instanceof Chest || blockData instanceof RedstoneWire || blockData instanceof Snow || blockData instanceof Stairs || blockData instanceof TrapDoor || blockData instanceof Piston || blockData instanceof PistonHead || blockData instanceof TechnicalPiston || blockData instanceof org.bukkit.block.data.Rail;
}
/**
* Update the block count in the rollback hash
*
* @param userString
* The username for this rollback
* @param increment
* The amount to increment the block count by
*/
protected static void updateBlockCount(String userString, int increment) {
int[] rollbackHashData = ConfigHandler.rollbackHash.get(userString);
int itemCount = rollbackHashData[0];
int blockCount = rollbackHashData[1];
int entityCount = rollbackHashData[2];
int scannedWorlds = rollbackHashData[4];
blockCount += increment;
ConfigHandler.rollbackHash.put(userString, new int[] { itemCount, blockCount, entityCount, 0, scannedWorlds });
}
/**
* Apply all pending block changes to the world
*
@@ -0,0 +1,32 @@
package net.coreprotect.database.rollback;
public final class RollbackCounters {
private int items;
private int blocks;
private int entities;
public void addItems(int amount) {
items += amount;
}
public void addBlocks(int amount) {
blocks += amount;
}
public void addEntities(int amount) {
entities += amount;
}
public int getItems() {
return items;
}
public int getBlocks() {
return blocks;
}
public int getEntities() {
return entities;
}
}
@@ -2,7 +2,6 @@ package net.coreprotect.database.rollback;
import java.util.Arrays;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
@@ -13,7 +12,6 @@ import org.bukkit.entity.EntityType;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.WorldUtils;
import net.coreprotect.utility.ErrorReporter;
public class RollbackEntityHandler {
@@ -21,12 +19,8 @@ public class RollbackEntityHandler {
/**
* Processes an entity-related rollback operation.
*
* @param row
* The database row containing entity data (used only for specific operations)
* @param rollbackType
* The type of rollback (0 for rollback, 1 for restore)
* @param finalUserString
* The user string for tracking operations
* @param bukkitWorld
* The world the entity change belongs to
* @param rowTypeRaw
* The raw type value
* @param rowData
@@ -43,26 +37,14 @@ public class RollbackEntityHandler {
* The Z coordinate
* @param rowWorldId
* The world ID
* @param rowUserId
* The user ID
* @param rowUser
* The username associated with this entity change
* @return The number of entities affected (1 if successful, 0 otherwise)
*/
public static int processEntity(Object[] row, int rollbackType, String finalUserString, int oldTypeRaw, int rowTypeRaw, int rowData, int rowAction, int rowRolledBack, int rowX, int rowY, int rowZ, int rowWorldId, int rowUserId, String rowUser) {
public static int processEntity(World bukkitWorld, int oldTypeRaw, int rowTypeRaw, int rowData, int rowAction, int rowRolledBack, int rowX, int rowY, int rowZ, int rowWorldId, String rowUser) {
try {
// Entity kill
if (rowAction == 3) {
String world = getWorldName(rowWorldId);
if (world.isEmpty()) {
return 0;
}
World bukkitWorld = Bukkit.getServer().getWorld(world);
if (bukkitWorld == null) {
return 0;
}
Block block = bukkitWorld.getBlockAt(rowX, rowY, rowZ);
if (!bukkitWorld.isChunkLoaded(block.getChunk())) {
bukkitWorld.getChunkAt(block.getLocation());
@@ -74,7 +56,6 @@ public class RollbackEntityHandler {
EntityType entityType = EntityUtils.getEntityType(rowTypeRaw);
// Use the spawnEntity method from the RollbackUtil class instead of Queue
spawnEntity(rowUser, block.getState(), entityType, rowData);
updateEntityCount(finalUserString, 1);
return 1;
}
}
@@ -90,10 +71,10 @@ public class RollbackEntityHandler {
int zmin = rowZ - 5;
int zmax = rowZ + 5;
boolean removed = removeMatchingEntity(Arrays.asList(block.getChunk().getEntities()), finalUserString, entityId, targetType, xmin, xmax, ymin, ymax, zmin, zmax, true);
boolean removed = removeMatchingEntity(Arrays.asList(block.getChunk().getEntities()), entityId, targetType, xmin, xmax, ymin, ymax, zmin, zmax, true);
if (!removed && entityId > -1 && !ConfigHandler.isFolia) {
// On non-Folia, keep world-wide fallback for moved entities.
removed = removeMatchingEntity(block.getWorld().getLivingEntities(), finalUserString, entityId, targetType, xmin, xmax, ymin, ymax, zmin, zmax, false);
removed = removeMatchingEntity(block.getWorld().getLivingEntities(), entityId, targetType, xmin, xmax, ymin, ymax, zmin, zmax, false);
}
if (removed) {
@@ -110,17 +91,6 @@ public class RollbackEntityHandler {
return 0;
}
/**
* Gets the world name from a world ID.
*
* @param worldId
* The world ID
* @return The world name
*/
private static String getWorldName(int 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;
@@ -131,11 +101,10 @@ public class RollbackEntityHandler {
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) {
private static boolean removeMatchingEntity(Iterable<? extends Entity> entities, 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;
}
@@ -149,7 +118,6 @@ public class RollbackEntityHandler {
continue;
}
updateEntityCount(userString, 1);
entity.remove();
return true;
}
@@ -163,29 +131,6 @@ public class RollbackEntityHandler {
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.
*
* @param userString
* The user string identifier
* @param increment
* The amount to increment the entity count by
*/
public static void updateEntityCount(String userString, int increment) {
int[] rollbackHashData = ConfigHandler.rollbackHash.get(userString);
if (rollbackHashData != null) {
int itemCount = rollbackHashData[0];
int blockCount = rollbackHashData[1];
int entityCount = rollbackHashData[2];
int next = rollbackHashData[3];
int scannedWorlds = rollbackHashData[4];
entityCount += increment;
ConfigHandler.rollbackHash.put(userString, new int[] { itemCount, blockCount, entityCount, next, scannedWorlds });
}
}
/**
* Spawns an entity at the given block location.
*
@@ -35,11 +35,9 @@ import net.coreprotect.model.PendingBlockChange;
import net.coreprotect.model.item.ItemTransactionActions;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.BlockTypeUtils;
import net.coreprotect.utility.ItemUtils;
import net.coreprotect.utility.MaterialUtils;
import net.coreprotect.utility.Teleport;
import net.coreprotect.utility.WorldUtils;
import net.coreprotect.utility.ErrorReporter;
public class RollbackProcessor {
@@ -73,26 +71,22 @@ public class RollbackProcessor {
* The user performing the rollback
* @param bukkitRollbackWorld
* The world to process
* @param blockDataCache
* The rollback-scoped BlockData parse cache
* @return True if successful, false if there was an error
*/
public static boolean processChunk(int finalChunkX, int finalChunkZ, long chunkKey, ArrayList<Object[]> blockList, ArrayList<Object[]> itemList, int rollbackType, int preview, String finalUserString, Player finalUser, World bukkitRollbackWorld, boolean inventoryRollback) {
public static boolean processChunk(int finalChunkX, int finalChunkZ, long chunkKey, ArrayList<Object[]> blockList, ArrayList<Object[]> itemList, int rollbackType, int preview, String finalUserString, Player finalUser, World bukkitRollbackWorld, boolean inventoryRollback, RollbackBlockDataCache blockDataCache) {
RollbackCounters counters = new RollbackCounters();
try {
boolean clearInventories = Config.getGlobal().ROLLBACK_ITEMS;
ArrayList<Object[]> data = blockList != null ? blockList : new ArrayList<>();
ArrayList<Object[]> itemData = itemList != null ? itemList : new ArrayList<>();
Map<Block, PendingBlockChange> chunkChanges = new LinkedHashMap<>();
Map<String, BlockData> parsedBlockDataCache = new HashMap<>();
Map<Integer, BlockData> defaultBlockDataCache = new HashMap<>();
loadChunk(bukkitRollbackWorld, finalChunkX, finalChunkZ, inventoryRollback);
// Process blocks
for (Object[] row : data) {
int[] rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString);
int itemCount = rollbackHashData[0];
int blockCount = rollbackHashData[1];
int entityCount = rollbackHashData[2];
int scannedWorlds = rollbackHashData[4];
int rowX = (Integer) row[3];
int rowY = (Integer) row[4];
int rowZ = (Integer) row[5];
@@ -111,13 +105,13 @@ public class RollbackProcessor {
meta = RollbackUtil.deserializeMetadata(rowMeta);
}
BlockData blockData = getParsedBlockData(blockDataString, parsedBlockDataCache);
BlockData blockData = blockDataCache.getParsedBlockData(blockDataString);
BlockData rawBlockData = null;
if (blockData != null) {
rawBlockData = blockData.clone();
}
if (rawBlockData == null) {
rawBlockData = getDefaultBlockData(rowTypeRaw, defaultBlockDataCache);
rawBlockData = blockDataCache.getDefaultBlockData(rowTypeRaw);
}
if (rowType == Material.NOTE_BLOCK) {
normalizeRollbackBlockData(blockData);
@@ -148,50 +142,30 @@ public class RollbackProcessor {
}
if (preview > 0) {
if (rowAction != 3) { // entity kill
String world = WorldUtils.getWorldName(rowWorldId);
if (world.length() == 0) {
continue;
}
World bukkitWorld = Bukkit.getServer().getWorld(world);
if (bukkitWorld == null) {
continue;
}
Block block = bukkitWorld.getBlockAt(rowX, rowY, rowZ);
Block block = bukkitRollbackWorld.getBlockAt(rowX, rowY, rowZ);
if (preview == 2) {
Material blockType = block.getType();
if (!BukkitAdapter.ADAPTER.isItemFrame(blockType) && !blockType.equals(Material.PAINTING) && !blockType.equals(Material.ARMOR_STAND) && !blockType.equals(Material.END_CRYSTAL)) {
BlockUtils.prepareTypeAndData(chunkChanges, block, blockType, block.getBlockData(), true);
blockCount++;
counters.addBlocks(1);
}
}
else {
if ((!BukkitAdapter.ADAPTER.isItemFrame(rowType)) && (rowType != Material.PAINTING) && (rowType != Material.ARMOR_STAND) && (rowType != Material.END_CRYSTAL)) {
BlockUtils.prepareTypeAndData(chunkChanges, block, rowType, blockData, true);
blockCount++;
counters.addBlocks(1);
}
}
}
else {
entityCount++;
counters.addEntities(1);
}
}
else if (rowAction == 3) { // entity kill
entityCount += RollbackEntityHandler.processEntity(row, rollbackType, finalUserString, oldTypeRaw, rowTypeRaw, rowData, rowAction, MaterialUtils.rolledBack((Integer) row[9], false), rowX, rowY, rowZ, rowWorldId, (Integer) row[2], rowUser);
counters.addEntities(RollbackEntityHandler.processEntity(bukkitRollbackWorld, oldTypeRaw, rowTypeRaw, rowData, rowAction, rowRolledBack, rowX, rowY, rowZ, rowWorldId, rowUser));
}
else {
String world = WorldUtils.getWorldName(rowWorldId);
if (world.length() == 0) {
continue;
}
World bukkitWorld = Bukkit.getServer().getWorld(world);
if (bukkitWorld == null) {
continue;
}
Block block = bukkitWorld.getBlockAt(rowX, rowY, rowZ);
Block block = bukkitRollbackWorld.getBlockAt(rowX, rowY, rowZ);
boolean changeBlock = true;
boolean countBlock = true;
@@ -241,12 +215,10 @@ public class RollbackProcessor {
}
}
if (RollbackBlockHandler.processBlockChange(bukkitWorld, block, row, rollbackType, clearInventories, chunkChanges, countBlock, oldTypeMaterial, pendingChangeType, pendingChangeData, finalUserString, rawBlockData, changeType, changeBlock, changeBlockData, meta != null ? new ArrayList<>(meta) : null, blockData, rowUser, rowType, rowX, rowY, rowZ, rowTypeRaw, rowData, rowAction, rowWorldId, BlockUtils.byteDataToString((byte[]) row[13], rowTypeRaw)) && countBlock) {
blockCount++;
if (RollbackBlockHandler.processBlockChange(bukkitRollbackWorld, block, row, rollbackType, clearInventories, chunkChanges, countBlock, oldTypeMaterial, pendingChangeType, pendingChangeData, counters, rawBlockData, changeType, changeBlock, changeBlockData, meta != null ? new ArrayList<>(meta) : null, blockData, rowUser, rowType, rowX, rowY, rowZ, rowTypeRaw, rowData, rowAction, rowWorldId, BlockUtils.byteDataToString((byte[]) row[13], rowTypeRaw)) && countBlock) {
counters.addBlocks(1);
}
}
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount, blockCount, entityCount, 0, scannedWorlds });
}
data.clear();
@@ -265,11 +237,6 @@ public class RollbackProcessor {
String lastFace = "";
for (Object[] row : itemData) {
int[] rollbackHashData1 = ConfigHandler.rollbackHash.get(finalUserString);
int itemCount1 = rollbackHashData1[0];
int blockCount1 = rollbackHashData1[1];
int entityCount1 = rollbackHashData1[2];
int scannedWorlds = rollbackHashData1[4];
int rowX = (Integer) row[3];
int rowY = (Integer) row[4];
int rowZ = (Integer) row[5];
@@ -322,8 +289,7 @@ public class RollbackProcessor {
sortPlayers.put(player, currentSortList);
}
itemCount1 = itemCount1 + rowAmount;
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount1, blockCount1, entityCount1, 0, scannedWorlds });
counters.addItems(rowAmount);
continue; // remove this for merged rollbacks in future? (be sure to re-enable chunk sorting)
}
@@ -338,16 +304,7 @@ public class RollbackProcessor {
if (!containerInit || rowX != lastX || rowY != lastY || rowZ != lastZ || rowWorldId != lastWorldId || !faceData.equals(lastFace)) {
container = null; // container patch 2.14.0
String world = WorldUtils.getWorldName(rowWorldId);
if (world.length() == 0) {
continue;
}
World bukkitWorld = Bukkit.getServer().getWorld(world);
if (bukkitWorld == null) {
continue;
}
Block block = bukkitWorld.getBlockAt(rowX, rowY, rowZ);
Block block = bukkitRollbackWorld.getBlockAt(rowX, rowY, rowZ);
if (BlockGroup.CONTAINERS.contains(block.getType())) {
BlockState blockState = block.getState();
@@ -399,13 +356,11 @@ public class RollbackProcessor {
itemstack = (ItemStack) populatedStack[2];
RollbackUtil.modifyContainerItems(containerType, container, slot, itemstack, action);
itemCount1 = itemCount1 + rowAmount;
counters.addItems(rowAmount);
}
containerInit = true;
}
}
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount1, blockCount1, entityCount1, 0, scannedWorlds });
}
itemData.clear();
@@ -414,12 +369,7 @@ public class RollbackProcessor {
}
sortPlayers.clear();
int[] rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString);
int itemCount = rollbackHashData[0];
int blockCount = rollbackHashData[1];
int entityCount = rollbackHashData[2];
int scannedWorlds = rollbackHashData[4];
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount, blockCount, entityCount, 1, (scannedWorlds + 1) });
updateRollbackHash(finalUserString, counters, 1);
// Teleport players out of danger if they're within this chunk
if (preview == 0) {
@@ -439,17 +389,20 @@ public class RollbackProcessor {
}
catch (Exception e) {
ErrorReporter.report(e);
int[] rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString);
int itemCount = rollbackHashData[0];
int blockCount = rollbackHashData[1];
int entityCount = rollbackHashData[2];
int scannedWorlds = rollbackHashData[4];
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount, blockCount, entityCount, 2, (scannedWorlds + 1) });
updateRollbackHash(finalUserString, counters, 2);
return false;
}
}
private static void updateRollbackHash(String finalUserString, RollbackCounters counters, int status) {
int[] rollbackHashData = ConfigHandler.rollbackHash.get(finalUserString);
int itemCount = rollbackHashData[0] + counters.getItems();
int blockCount = rollbackHashData[1] + counters.getBlocks();
int entityCount = rollbackHashData[2] + counters.getEntities();
int scannedWorlds = rollbackHashData[4] + 1;
ConfigHandler.rollbackHash.put(finalUserString, new int[] { itemCount, blockCount, entityCount, status, scannedWorlds });
}
private static void loadChunk(World world, int chunkX, int chunkZ, boolean inventoryRollback) {
if (inventoryRollback || world == null) {
return;
@@ -460,37 +413,4 @@ public class RollbackProcessor {
}
}
private static BlockData getParsedBlockData(String blockDataString, Map<String, BlockData> parsedBlockDataCache) {
if (blockDataString == null || !blockDataString.contains(":")) {
return null;
}
if (!parsedBlockDataCache.containsKey(blockDataString)) {
BlockData blockData = null;
try {
blockData = BlockTypeUtils.createBlockDataFromString(blockDataString);
if (blockData == null) {
blockData = Bukkit.getServer().createBlockData(blockDataString);
}
}
catch (Exception e) {
// corrupt BlockData, let the server automatically set the BlockData instead
}
parsedBlockDataCache.put(blockDataString, blockData);
}
return cloneBlockData(parsedBlockDataCache.get(blockDataString));
}
private static BlockData getDefaultBlockData(int rowTypeRaw, Map<Integer, BlockData> defaultBlockDataCache) {
if (!defaultBlockDataCache.containsKey(rowTypeRaw)) {
defaultBlockDataCache.put(rowTypeRaw, BlockUtils.createBlockData(rowTypeRaw));
}
return cloneBlockData(defaultBlockDataCache.get(rowTypeRaw));
}
private static BlockData cloneBlockData(BlockData blockData) {
return blockData != null ? blockData.clone() : null;
}
}
@@ -7,6 +7,7 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.block.Sign;
import org.bukkit.block.Skull;
import org.bukkit.entity.Entity;
@@ -77,6 +78,11 @@ public class PaperAdapter implements PaperInterface {
return false;
}
@Override
public double getAverageTickTime(Server server) {
return -1.0D;
}
@Override
public String getLine(Sign sign, int line) {
return BukkitAdapter.ADAPTER.getLine(sign, line);
@@ -87,6 +93,11 @@ public class PaperAdapter implements PaperInterface {
entity.teleport(location);
}
@Override
public void prefetchChunk(World world, int chunkX, int chunkZ) {
// chunk prefetching requires the Paper async chunk API
}
@Override
public String getSkullOwner(Skull skull) {
OfflinePlayer player = skull.getOwningPlayer();
@@ -9,6 +9,7 @@ import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Villager;
import org.bukkit.inventory.Inventory;
@@ -19,17 +20,39 @@ import net.coreprotect.model.entity.VillagerReputationData;
public class PaperHandler extends PaperAdapter {
private volatile boolean supportsSnapshotHolderLookup = true;
private volatile boolean supportsAverageTickTime = true;
@Override
public boolean isStopping(Server server) {
return server.isStopping();
}
@Override
public double getAverageTickTime(Server server) {
if (supportsAverageTickTime) {
try {
return server.getAverageTickTime();
}
catch (LinkageError | UnsupportedOperationException ignored) {
supportsAverageTickTime = false;
}
}
return -1.0D;
}
@Override
public void teleportAsync(Entity entity, Location location) {
entity.teleportAsync(location);
}
@Override
public void prefetchChunk(World world, int chunkX, int chunkZ) {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
world.getChunkAtAsync(chunkX, chunkZ);
}
}
@Override
public InventoryHolder getHolder(Inventory holder, boolean useSnapshot) {
if (supportsSnapshotHolderLookup) {
@@ -4,6 +4,7 @@ import java.util.List;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.block.Sign;
import org.bukkit.block.Skull;
import org.bukkit.entity.Entity;
@@ -18,10 +19,14 @@ public interface PaperInterface {
public boolean isStopping(Server server);
public double getAverageTickTime(Server server);
public String getLine(Sign sign, int line);
public void teleportAsync(Entity entity, Location location);
public void prefetchChunk(World world, int chunkX, int chunkZ);
public String getSkullOwner(Skull skull);
public String getSkullSkin(Skull skull);
@@ -19,6 +19,7 @@ import net.coreprotect.listener.ListenerHandler;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.thread.NetworkHandler;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.thread.TickTimeMonitor;
import net.coreprotect.utility.Chat;
import net.coreprotect.utility.ChatUtils;
import net.coreprotect.utility.Extensions;
@@ -149,6 +150,9 @@ public class PluginInitializationService {
}
}, 0);
// Start tick time monitor (only used where native tick timings are unavailable)
TickTimeMonitor.initialize(plugin);
// Start cache cleanup thread
Thread cacheCleanUpThread = new Thread(new CacheHandler());
cacheCleanUpThread.start();
@@ -0,0 +1,57 @@
package net.coreprotect.thread;
import net.coreprotect.CoreProtect;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.paper.PaperAdapter;
public final class TickTimeMonitor {
private static final int MONITOR_INTERVAL_TICKS = 40;
private static final double HEALTHY_TICK_MILLIS = 50.0D;
private static final double LAGGING_SAMPLE_MILLIS = 51.0D;
private static final double ESTIMATE_FLOOR_MILLIS = 38.0D;
private static final double ESTIMATE_DECAY_MILLIS = 3.0D;
private static volatile double estimatedTickTime = -1.0D;
private static long lastSampleTime = -1L;
private TickTimeMonitor() {
throw new IllegalStateException("TickTimeMonitor class");
}
public static void initialize(CoreProtect plugin) {
if (ConfigHandler.isFolia || PaperAdapter.ADAPTER.getAverageTickTime(plugin.getServer()) >= 0.0D) {
return;
}
estimatedTickTime = -1.0D;
lastSampleTime = -1L;
Scheduler.scheduleSyncRepeatingTask(plugin, TickTimeMonitor::recordSample, null, MONITOR_INTERVAL_TICKS, MONITOR_INTERVAL_TICKS);
}
public static double getEstimatedTickTime() {
return estimatedTickTime;
}
private static void recordSample() {
long now = System.nanoTime();
if (lastSampleTime > 0) {
double sampleMillis = (now - lastSampleTime) / (MONITOR_INTERVAL_TICKS * 1_000_000.0D);
estimatedTickTime = nextEstimate(estimatedTickTime, sampleMillis);
}
lastSampleTime = now;
}
public static double nextEstimate(double currentEstimate, double sampleMillis) {
if (sampleMillis > LAGGING_SAMPLE_MILLIS) {
// Ticks are stretching past the 50ms pace, so the wall-clock tick time is the real workload
return sampleMillis;
}
// Ticks are keeping pace, which proves the workload is at most a full tick but hides any idle headroom below that;
// probe for headroom by decaying the estimate, stopping at the point that maps to a 37ms batch budget
double provenEstimate = currentEstimate > 0.0D ? Math.min(currentEstimate, HEALTHY_TICK_MILLIS) : HEALTHY_TICK_MILLIS;
return Math.max(ESTIMATE_FLOOR_MILLIS, provenEstimate - ESTIMATE_DECAY_MILLIS);
}
}