Rework the chunk deletion (#1897)

* delete island one by one

* register before IslandDeletionManager

* optimize imports

* setting

* just some indents

* config

* run synchronously

* a bit reformat before recoding

* proper delete chunks

* comment

* combine the task call

* expose the NMS Handler

* don't have to try-catch this

* we know that this is final

* expose copy chunk data so that it can be overridden

* Don't have to use Vector

* set block from minimum height

* remove NMS and use fallback if not set

* only get the height once

* fix test
This commit is contained in:
Huynh Tien 2022-01-02 08:38:27 +07:00 committed by GitHub
parent c9c9ea0389
commit ce1d8e5117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 215 additions and 142 deletions

13
pom.xml
View File

@ -172,7 +172,10 @@
<id>maven-snapshots</id>
<url>https://repository.apache.org/content/repositories/snapshots/</url>
</repository>
<repository>
<id>minecraft-repo</id>
<url>https://libraries.minecraft.net/</url>
</repository>
</repositories>
<dependencies>
@ -190,11 +193,11 @@
<version>${paper.version}</version>
<scope>provided</scope>
</dependency>
<!-- Spigot NMS. Used for Head Getter and chunk deletion. -->
<!-- AuthLib. Used for Head Getter. -->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot</artifactId>
<version>${spigot.version}</version>
<groupId>com.mojang</groupId>
<artifactId>authlib</artifactId>
<version>3.2.38</version>
<scope>provided</scope>
</dependency>
<!-- Metrics -->

View File

@ -30,19 +30,7 @@ import world.bentobox.bentobox.listeners.JoinLeaveListener;
import world.bentobox.bentobox.listeners.PanelListenerManager;
import world.bentobox.bentobox.listeners.PortalTeleportationListener;
import world.bentobox.bentobox.listeners.StandardSpawnProtectionListener;
import world.bentobox.bentobox.managers.AddonsManager;
import world.bentobox.bentobox.managers.BlueprintsManager;
import world.bentobox.bentobox.managers.CommandsManager;
import world.bentobox.bentobox.managers.FlagsManager;
import world.bentobox.bentobox.managers.HooksManager;
import world.bentobox.bentobox.managers.IslandDeletionManager;
import world.bentobox.bentobox.managers.IslandWorldManager;
import world.bentobox.bentobox.managers.IslandsManager;
import world.bentobox.bentobox.managers.LocalesManager;
import world.bentobox.bentobox.managers.PlaceholdersManager;
import world.bentobox.bentobox.managers.PlayersManager;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.bentobox.managers.WebManager;
import world.bentobox.bentobox.managers.*;
import world.bentobox.bentobox.util.heads.HeadGetter;
import world.bentobox.bentobox.versions.ServerCompatibility;
@ -69,6 +57,7 @@ public class BentoBox extends JavaPlugin {
private HooksManager hooksManager;
private PlaceholdersManager placeholdersManager;
private IslandDeletionManager islandDeletionManager;
private IslandChunkDeletionManager islandChunkDeletionManager;
private WebManager webManager;
// Settings
@ -294,6 +283,7 @@ public class BentoBox extends JavaPlugin {
// Death counter
manager.registerEvents(new DeathListener(this), this);
// Island Delete Manager
islandChunkDeletionManager = new IslandChunkDeletionManager(this);
islandDeletionManager = new IslandDeletionManager(this);
manager.registerEvents(islandDeletionManager, this);
}
@ -523,6 +513,13 @@ public class BentoBox extends JavaPlugin {
return islandDeletionManager;
}
/**
* @return the islandChunkDeletionManager
*/
public IslandChunkDeletionManager getIslandChunkDeletionManager() {
return islandChunkDeletionManager;
}
/**
* @return an optional of the Bstats instance
* @since 1.1

View File

@ -298,6 +298,15 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "island.deletion.keep-previous-island-on-reset", since = "1.13.0")
private boolean keepPreviousIslandOnReset = false;
@ConfigComment("Toggles how the islands are deleted.")
@ConfigComment("* If set to 'false', all islands will be deleted at once.")
@ConfigComment(" This is fast but may cause an impact on the performance")
@ConfigComment(" as it'll load all the chunks of the in-deletion islands.")
@ConfigComment("* If set to 'true', the islands will be deleted one by one.")
@ConfigComment(" This is slower but will not cause any impact on the performance.")
@ConfigEntry(path = "island.deletion.slow-deletion", since = "1.19.1")
private boolean slowDeletion = false;
@ConfigComment("By default, If the destination is not safe, the plugin will try to search for a safe spot around the destination,")
@ConfigComment("then it will try to expand the y-coordinate up and down from the destination.")
@ConfigComment("This setting limits how far the y-coordinate will be expanded.")
@ -905,4 +914,12 @@ public class Settings implements ConfigObject {
public void setSafeSpotSearchVerticalRange(int safeSpotSearchVerticalRange) {
this.safeSpotSearchVerticalRange = safeSpotSearchVerticalRange;
}
public boolean isSlowDeletion() {
return slowDeletion;
}
public void setSlowDeletion(boolean slowDeletion) {
this.slowDeletion = slowDeletion;
}
}

View File

@ -0,0 +1,60 @@
package world.bentobox.bentobox.managers;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.database.objects.IslandDeletion;
import world.bentobox.bentobox.util.DeleteIslandChunks;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicReference;
/**
* Manages the queue of island chunks to be deleted.
*/
public class IslandChunkDeletionManager implements Runnable {
private final boolean slowDeletion;
private final BentoBox plugin;
private final AtomicReference<DeleteIslandChunks> currentTask;
private final Queue<IslandDeletion> queue;
public IslandChunkDeletionManager(BentoBox plugin) {
this.plugin = plugin;
this.currentTask = new AtomicReference<>();
this.queue = new LinkedList<>();
this.slowDeletion = plugin.getSettings().isSlowDeletion();
if (slowDeletion) {
plugin.getServer().getScheduler().runTaskTimer(plugin, this, 0L, 20L);
}
}
@Override
public void run() {
if (queue.isEmpty()) {
return;
}
DeleteIslandChunks task = this.currentTask.get();
if (task != null && !task.isCompleted()) {
return;
}
IslandDeletion islandDeletion = queue.remove();
currentTask.set(startDeleteTask(islandDeletion));
}
private DeleteIslandChunks startDeleteTask(IslandDeletion islandDeletion) {
return new DeleteIslandChunks(plugin, islandDeletion);
}
/**
* Adds an island deletion to the queue.
*
* @param islandDeletion island deletion
*/
public void add(IslandDeletion islandDeletion) {
if (slowDeletion) {
queue.add(islandDeletion);
} else {
startDeleteTask(islandDeletion);
}
}
}

View File

@ -16,7 +16,6 @@ import world.bentobox.bentobox.api.events.island.IslandDeleteChunksEvent;
import world.bentobox.bentobox.api.events.island.IslandDeletedEvent;
import world.bentobox.bentobox.database.Database;
import world.bentobox.bentobox.database.objects.IslandDeletion;
import world.bentobox.bentobox.util.DeleteIslandChunks;
import world.bentobox.bentobox.util.Util;
/**
@ -57,7 +56,7 @@ public class IslandDeletionManager implements Listener {
} else {
plugin.log("Resuming deletion of island at " + di.getLocation().getWorld().getName() + " " + Util.xyz(di.getLocation().toVector()));
inDeletion.add(di.getLocation());
new DeleteIslandChunks(plugin, di);
plugin.getIslandChunkDeletionManager().add(di);
}
});
}

View File

@ -54,7 +54,6 @@ import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.database.objects.IslandDeletion;
import world.bentobox.bentobox.lists.Flags;
import world.bentobox.bentobox.managers.island.IslandCache;
import world.bentobox.bentobox.util.DeleteIslandChunks;
import world.bentobox.bentobox.util.Util;
import world.bentobox.bentobox.util.teleport.SafeSpotTeleport;
@ -346,7 +345,7 @@ public class IslandsManager {
// Remove players from island
removePlayersFromIsland(island);
// Remove blocks from world
new DeleteIslandChunks(plugin, new IslandDeletion(island));
plugin.getIslandChunkDeletionManager().add(new IslandDeletion(island));
}
}

View File

@ -2,9 +2,37 @@ package world.bentobox.bentobox.nms;
import org.bukkit.Chunk;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.util.BoundingBox;
public interface NMSAbstraction {
/**
* Copy the chunk data and biome grid to the given chunk.
* @param chunk - chunk to copy to
* @param chunkData - chunk data to copy
* @param biomeGrid - biome grid to copy to
* @param limitBox - bounding box to limit the copying
*/
default void copyChunkDataToChunk(Chunk chunk, ChunkGenerator.ChunkData chunkData, ChunkGenerator.BiomeGrid biomeGrid, BoundingBox limitBox) {
double baseX = chunk.getX() << 4;
double baseZ = chunk.getZ() << 4;
int minHeight = chunk.getWorld().getMinHeight();
int maxHeight = chunk.getWorld().getMaxHeight();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (!limitBox.contains(baseX + x, 0, baseZ + z)) {
continue;
}
for (int y = minHeight; y < maxHeight; y++) {
setBlockInNativeChunk(chunk, x, y, z, chunkData.getBlockData(x, y, z), false);
// 3D biomes, 4 blocks separated
if (x % 4 == 0 && y % 4 == 0 && z % 4 == 0) {
chunk.getBlock(x, y, z).setBiome(biomeGrid.getBiome(x, y, z));
}
}
}
}
}
/**
* Update the low-level chunk information for the given block to the new block ID and data. This

View File

@ -1,31 +0,0 @@
package world.bentobox.bentobox.nms.v1_18_R1;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_18_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_18_R1.block.data.CraftBlockData;
import net.minecraft.core.BlockPosition;
import net.minecraft.world.level.World;
import net.minecraft.world.level.block.state.IBlockData;
import net.minecraft.world.level.chunk.Chunk;
import world.bentobox.bentobox.nms.NMSAbstraction;
public class NMSHandler implements NMSAbstraction {
private static final IBlockData AIR = ((CraftBlockData) Bukkit.createBlockData(Material.AIR)).getState();
@Override
public void setBlockInNativeChunk(org.bukkit.Chunk chunk, int x, int y, int z, BlockData blockData, boolean applyPhysics) {
CraftBlockData craft = (CraftBlockData) blockData;
World nmsWorld = ((CraftWorld) chunk.getWorld()).getHandle();
Chunk nmsChunk = nmsWorld.d(chunk.getX(), chunk.getZ());
BlockPosition bp = new BlockPosition((chunk.getX() << 4) + x, y, (chunk.getZ() << 4) + z);
// Setting the block to air before setting to another state prevents some console errors
nmsChunk.a(bp, AIR, applyPhysics);
nmsChunk.a(bp, craft.getState(), applyPhysics);
}
}

View File

@ -1,9 +1,6 @@
package world.bentobox.bentobox.util;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
@ -13,8 +10,6 @@ import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.ChunkGenerator.ChunkData;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.scheduler.BukkitTask;
import io.papermc.lib.PaperLib;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.events.island.IslandEvent;
@ -22,6 +17,13 @@ import world.bentobox.bentobox.api.events.island.IslandEvent.Reason;
import world.bentobox.bentobox.database.objects.IslandDeletion;
import world.bentobox.bentobox.nms.NMSAbstraction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Deletes islands chunk by chunk
*
@ -29,21 +31,23 @@ import world.bentobox.bentobox.nms.NMSAbstraction;
*/
public class DeleteIslandChunks {
private final IslandDeletion di;
private final BentoBox plugin;
private final World netherWorld;
private final World endWorld;
private final AtomicBoolean completed;
private final NMSAbstraction nms;
private int chunkX;
private int chunkZ;
private BukkitTask task;
private final IslandDeletion di;
private boolean inDelete;
private final BentoBox plugin;
private NMSAbstraction nms;
private final World netherWorld;
private final World endWorld;
private CompletableFuture<Void> currentTask = CompletableFuture.completedFuture(null);
public DeleteIslandChunks(BentoBox plugin, IslandDeletion di) {
this.plugin = plugin;
this.chunkX = di.getMinXChunk();
this.chunkZ = di.getMinZChunk();
this.di = di;
completed = new AtomicBoolean(false);
// Nether
if (plugin.getIWM().isNetherGenerate(di.getWorld()) && plugin.getIWM().isNetherIslands(di.getWorld())) {
netherWorld = plugin.getIWM().getNetherWorld(di.getWorld());
@ -57,9 +61,8 @@ public class DeleteIslandChunks {
endWorld = null;
}
// NMS
try {
this.nms = Util.getNMS();
} catch (Exception e) {
this.nms = Util.getNMS();
if (nms == null) {
plugin.logError("Could not delete chunks because of NMS error");
return;
}
@ -73,55 +76,58 @@ public class DeleteIslandChunks {
private void regenerateChunks() {
// Run through all chunks of the islands and regenerate them.
task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (inDelete) return;
inDelete = true;
if (!currentTask.isDone()) return;
if (isEnded(chunkX)) {
finish();
return;
}
List<CompletableFuture<Void>> newTasks = new ArrayList<>();
for (int i = 0; i < plugin.getSettings().getDeleteSpeed(); i++) {
boolean last = i == plugin.getSettings().getDeleteSpeed() -1;
if (isEnded(chunkX)) {
break;
}
final int x = chunkX;
final int z = chunkZ;
plugin.getIWM().getAddon(di.getWorld()).ifPresent(gm ->
// Overworld
processChunk(gm, di.getWorld(), x, z).thenRun(() ->
// Nether
processChunk(gm, netherWorld, x, z).thenRun(() ->
// End
processChunk(gm, endWorld, x, z).thenRun(() -> finish(last, x)))));
plugin.getIWM().getAddon(di.getWorld()).ifPresent(gm -> {
newTasks.add(processChunk(gm, di.getWorld(), x, z)); // Overworld
newTasks.add(processChunk(gm, netherWorld, x, z)); // Nether
newTasks.add(processChunk(gm, endWorld, x, z)); // End
});
chunkZ++;
if (chunkZ > di.getMaxZChunk()) {
chunkZ = di.getMinZChunk();
chunkX++;
}
}
currentTask = CompletableFuture.allOf(newTasks.toArray(new CompletableFuture[0]));
}, 0L, 20L);
}
private void finish(boolean last, int x) {
if (x > di.getMaxXChunk()) {
// Fire event
IslandEvent.builder().deletedIslandInfo(di).reason(Reason.DELETED).build();
// We're done
task.cancel();
}
if (last) {
inDelete = false;
}
private boolean isEnded(int chunkX) {
return chunkX > di.getMaxXChunk();
}
private CompletableFuture<Boolean> processChunk(GameModeAddon gm, World world, int x, int z) {
private void finish() {
// Fire event
IslandEvent.builder().deletedIslandInfo(di).reason(Reason.DELETED).build();
// We're done
completed.set(true);
task.cancel();
}
private CompletableFuture<Void> processChunk(GameModeAddon gm, World world, int x, int z) {
if (world != null) {
CompletableFuture<Boolean> r = new CompletableFuture<>();
PaperLib.getChunkAtAsync(world, x, z).thenAccept(chunk -> regenerateChunk(r, gm, chunk, x, z));
return r;
return PaperLib.getChunkAtAsync(world, x, z).thenAccept(chunk -> regenerateChunk(gm, chunk, x, z));
} else {
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.completedFuture(false);
}
private void regenerateChunk(CompletableFuture<Boolean> r, GameModeAddon gm, Chunk chunk, int x, int z) {
private void regenerateChunk(GameModeAddon gm, Chunk chunk, int x, int z) {
// Clear all inventories
Arrays.stream(chunk.getTileEntities()).filter(te -> (te instanceof InventoryHolder))
.filter(te -> di.inBounds(te.getLocation().getBlockX(), te.getLocation().getBlockZ()))
.forEach(te -> ((InventoryHolder)te).getInventory().clear());
Arrays.stream(chunk.getTileEntities()).filter(InventoryHolder.class::isInstance)
.filter(te -> di.inBounds(te.getLocation().getBlockX(), te.getLocation().getBlockZ()))
.forEach(te -> ((InventoryHolder) te).getInventory().clear());
// Remove all entities
for (Entity e : chunk.getEntities()) {
if (!(e instanceof Player)) {
@ -133,30 +139,18 @@ public class DeleteIslandChunks {
ChunkGenerator cg = gm.getDefaultWorldGenerator(chunk.getWorld().getName(), "delete");
// Will be null if use-own-generator is set to true
if (cg != null) {
ChunkData cd = cg.generateChunkData(chunk.getWorld(), new Random(), chunk.getX(), chunk.getZ(), grid);
createChunk(cd, chunk, grid);
}
r.complete(true);
}
private void createChunk(ChunkData cd, Chunk chunk, MyBiomeGrid grid) {
int baseX = chunk.getX() << 4;
int baseZ = chunk.getZ() << 4;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (di.inBounds(baseX + x, baseZ + z)) {
for (int y = 0; y < chunk.getWorld().getMaxHeight(); y++) {
nms.setBlockInNativeChunk(chunk, x, y, z, cd.getBlockData(x, y, z), false);
// 3D biomes, 4 blocks separated
if (x%4 == 0 && y%4 == 0 && z%4 == 0) {
chunk.getBlock(x, y, z).setBiome(grid.getBiome(x, y, z));
}
}
}
}
}
nms.copyChunkDataToChunk(chunk, cd, grid, di.getBox());
// Remove all entities in chunk, including any dropped items as a result of clearing the blocks above
Arrays.stream(chunk.getEntities()).filter(e -> !(e instanceof Player) && di.inBounds(e.getLocation().getBlockX(), e.getLocation().getBlockZ())).forEach(Entity::remove);
}
public boolean isCompleted() {
return completed.get();
}
}

View File

@ -1,6 +1,5 @@
package world.bentobox.bentobox.util;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -65,6 +64,7 @@ public class Util {
private static final String THE_END = "_the_end";
private static String serverVersion = null;
private static BentoBox plugin = BentoBox.getInstance();
private static NMSAbstraction nms = null;
private Util() {}
@ -689,33 +689,23 @@ public class Util {
}
/**
* Checks what version the server is running and picks the appropriate NMS handler, or fallback
* @return an NMS accelerated class for this server, or a fallback Bukkit API based one
* @throws ClassNotFoundException - thrown if there is no fallback class - should never be thrown
* @throws SecurityException - thrown if security violation - should never be thrown
* @throws NoSuchMethodException - thrown if no constructor for NMS package
* @throws InvocationTargetException - should never be thrown
* @throws IllegalArgumentException - should never be thrown
* @throws IllegalAccessException - should never be thrown
* @throws InstantiationException - should never be thrown
* Set the NMS handler the plugin will use
* @param nms the NMS handler
*/
public static NMSAbstraction getNMS() throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
String serverPackageName = Bukkit.getServer().getClass().getPackage().getName();
String pluginPackageName = plugin.getClass().getPackage().getName();
String version = serverPackageName.substring(serverPackageName.lastIndexOf('.') + 1);
Class<?> clazz;
try {
clazz = Class.forName(pluginPackageName + ".nms." + version + ".NMSHandler");
} catch (Exception e) {
plugin.logWarning("No NMS Handler found for " + version + ", falling back to Bukkit API.");
clazz = Class.forName(pluginPackageName + ".nms.fallback.NMSHandler");
}
// Check if we have a NMSAbstraction implementing class at that location.
if (NMSAbstraction.class.isAssignableFrom(clazz)) {
return (NMSAbstraction) clazz.getConstructor().newInstance();
} else {
throw new IllegalStateException("Class " + clazz.getName() + " does not implement NMSAbstraction");
public static void setNms(NMSAbstraction nms) {
Util.nms = nms;
}
/**
* Get the NMS handler the plugin will use
* @return an NMS accelerated class for this server
*/
public static NMSAbstraction getNMS() {
if (nms == null) {
plugin.log("No NMS Handler was set, falling back to Bukkit API.");
setNms(new world.bentobox.bentobox.nms.fallback.NMSHandler());
}
return nms;
}
/**

View File

@ -195,6 +195,14 @@ island:
# This is the default behaviour.
# Added since 1.13.0.
keep-previous-island-on-reset: false
# Toggles how the islands are deleted.
# * If set to 'false', all islands will be deleted at once.
# This is fast but may cause an impact on the performance
# as it'll load all the chunks of the in-deletion islands.
# * If set to 'true', the islands will be deleted one by one.
# This is slower but will not cause any impact on the performance.
# Added since 1.19.1.
slow-deletion: false
# By default, If the destination is not safe, the plugin will try to search for a safe spot around the destination,
# then it will try to expand the y-coordinate up and down from the destination.
# This setting limits how far the y-coordinate will be expanded.

View File

@ -67,6 +67,8 @@ public class IslandDeletionManagerTest {
private BukkitScheduler scheduler;
@Mock
private IslandWorldManager iwm;
@Mock
private IslandChunkDeletionManager chunkDeletionManager;
/**
@ -101,6 +103,8 @@ public class IslandDeletionManagerTest {
// IWM
when(plugin.getIWM()).thenReturn(iwm);
when(iwm.getIslandDistance(any())).thenReturn(64);
// Chunk deletion manager
when(plugin.getIslandChunkDeletionManager()).thenReturn(chunkDeletionManager);
// Island Deletion Manager
idm = new IslandDeletionManager(plugin);

View File

@ -108,6 +108,8 @@ public class IslandsManagerTest {
@Mock
private IslandWorldManager iwm;
@Mock
private IslandChunkDeletionManager chunkDeletionManager;
@Mock
private IslandCache islandCache;
private Optional<Island> optionalIsland;
@Mock
@ -158,6 +160,9 @@ public class IslandsManagerTest {
when(iwm.inWorld(any(Location.class))).thenReturn(true);
when(plugin.getIWM()).thenReturn(iwm);
// Chunk deletion manager
when(plugin.getIslandChunkDeletionManager()).thenReturn(chunkDeletionManager);
// Settings
Settings s = mock(Settings.class);
when(plugin.getSettings()).thenReturn(s);