diff --git a/api/src/main/java/net/momirealms/customcrops/api/core/block/CropBlock.java b/api/src/main/java/net/momirealms/customcrops/api/core/block/CropBlock.java index f2d618fe..1e38e7da 100644 --- a/api/src/main/java/net/momirealms/customcrops/api/core/block/CropBlock.java +++ b/api/src/main/java/net/momirealms/customcrops/api/core/block/CropBlock.java @@ -394,7 +394,7 @@ private void tickCrop(CustomCropsBlockState state, CustomCropsWorld world, Po } } }, bukkitLocation); - }, plugin.getScheduler().async()); + }, world.scheduler().async()); } public int point(CustomCropsBlockState state) { diff --git a/api/src/main/java/net/momirealms/customcrops/api/core/world/CustomCropsWorld.java b/api/src/main/java/net/momirealms/customcrops/api/core/world/CustomCropsWorld.java index 43fbc3c1..138d524b 100644 --- a/api/src/main/java/net/momirealms/customcrops/api/core/world/CustomCropsWorld.java +++ b/api/src/main/java/net/momirealms/customcrops/api/core/world/CustomCropsWorld.java @@ -156,6 +156,13 @@ default CustomCropsRegion restoreRegion(RegionPos pos, ConcurrentHashMap implements CustomCropsWorld { private WorldSetting setting; private final WorldAdaptor adaptor; private final WorldExtraData extraData; + private final WorldScheduler scheduler; public CustomCropsWorldImpl(W world, WorldAdaptor adaptor) { this.world = new WeakReference<>(world); @@ -61,6 +62,7 @@ public CustomCropsWorldImpl(W world, WorldAdaptor adaptor) { this.adaptor = adaptor; this.extraData = adaptor.loadExtraData(world); this.currentMinecraftDay = (int) (bukkitWorld().getFullTime() / 24_000); + this.scheduler = new WorldScheduler(BukkitCustomCropsPlugin.getInstance()); } @NotNull @@ -140,6 +142,11 @@ public CustomCropsChunk[] lazyChunks() { return lazyChunks.values().toArray(new CustomCropsChunk[0]); } + @Override + public CustomCropsRegion[] loadedRegions() { + return loadedRegions.values().toArray(new CustomCropsRegion[0]); + } + @NotNull @Override public Optional getBlockState(Pos3 location) { @@ -181,7 +188,15 @@ public Optional addBlockState(Pos3 location, CustomCropsB } @Override - public void save() { + public void save(boolean async) { + if (async) { + this.scheduler.async().execute(this::save); + } else { + BukkitCustomCropsPlugin.getInstance().getScheduler().sync().run(this::save, null); + } + } + + private void save() { long time1 = System.currentTimeMillis(); this.adaptor.saveExtraData(this); for (CustomCropsChunk chunk : loadedChunks.values()) { @@ -201,7 +216,7 @@ public void save() { public void setTicking(boolean tick) { if (tick) { if (this.tickTask == null || this.tickTask.isCancelled()) - this.tickTask = BukkitCustomCropsPlugin.getInstance().getScheduler().asyncRepeating(this::timer, 1, 1, TimeUnit.SECONDS); + this.tickTask = this.scheduler.asyncRepeating(this::timer, 1, 1, TimeUnit.SECONDS); } else { if (this.tickTask != null && !this.tickTask.isCancelled()) this.tickTask.cancel(); @@ -257,7 +272,8 @@ private boolean isANewDay() { private void saveLazyRegions() { this.regionTimer++; - if (this.regionTimer >= 600) { + // To avoid the same timing as saving + if (this.regionTimer >= 666) { this.regionTimer = 0; ArrayList removed = new ArrayList<>(); for (Map.Entry entry : loadedRegions.entrySet()) { @@ -461,11 +477,12 @@ public CustomCropsRegion getOrCreateRegion(RegionPos regionPos) { } private boolean shouldUnloadRegion(RegionPos regionPos) { + World bukkitWorld = bukkitWorld(); for (int chunkX = regionPos.x() * 32; chunkX < regionPos.x() * 32 + 32; chunkX++) { for (int chunkZ = regionPos.z() * 32; chunkZ < regionPos.z() * 32 + 32; chunkZ++) { // if a chunk is unloaded, then it should not be in the loaded chunks map ChunkPos pos = ChunkPos.of(chunkX, chunkZ); - if (isChunkLoaded(pos) || this.lazyChunks.containsKey(pos)) { + if (isChunkLoaded(pos) || this.lazyChunks.containsKey(pos) || bukkitWorld.isChunkLoaded(chunkX, chunkZ)) { return false; } } @@ -492,8 +509,14 @@ public boolean unloadRegion(CustomCropsRegion region) { } } } - this.loadedRegions.remove(region.regionPos()); this.adaptor.saveRegion(this, region); + this.loadedRegions.remove(region.regionPos()); + BukkitCustomCropsPlugin.getInstance().debug(() -> "[" + worldName + "] " + "Region " + region.regionPos() + " unloaded."); return true; } + + @Override + public WorldScheduler scheduler() { + return scheduler; + } } diff --git a/api/src/main/java/net/momirealms/customcrops/api/core/world/WorldScheduler.java b/api/src/main/java/net/momirealms/customcrops/api/core/world/WorldScheduler.java new file mode 100644 index 00000000..97b11074 --- /dev/null +++ b/api/src/main/java/net/momirealms/customcrops/api/core/world/WorldScheduler.java @@ -0,0 +1,144 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.momirealms.customcrops.api.core.world; + +import net.momirealms.customcrops.common.plugin.CustomCropsPlugin; +import net.momirealms.customcrops.common.plugin.scheduler.SchedulerTask; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class WorldScheduler { + private static final int PARALLELISM = 1; + + private final CustomCropsPlugin plugin; + + private final ScheduledThreadPoolExecutor scheduler; + private final ForkJoinPool worker; + + public WorldScheduler(CustomCropsPlugin plugin) { + this.plugin = plugin; + + this.scheduler = new ScheduledThreadPoolExecutor(1, r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName("customcrops-world-scheduler"); + return thread; + }); + this.scheduler.setRemoveOnCancelPolicy(true); + this.scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.worker = new ForkJoinPool(PARALLELISM, new WorkerThreadFactory(), new ExceptionHandler(), false); + } + + public Executor async() { + return this.worker; + } + + public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) { + ScheduledFuture future = this.scheduler.schedule(() -> this.worker.execute(task), delay, unit); + return new JavaCancellable(future); + } + + public SchedulerTask asyncRepeating(Runnable task, long delay, long interval, TimeUnit unit) { + ScheduledFuture future = this.scheduler.scheduleAtFixedRate(() -> this.worker.execute(task), delay, interval, unit); + return new JavaCancellable(future); + } + + public void shutdownScheduler() { + this.scheduler.shutdown(); + try { + if (!this.scheduler.awaitTermination(1, TimeUnit.MINUTES)) { + this.plugin.getPluginLogger().severe("Timed out waiting for the CustomCrops scheduler to terminate"); + reportRunningTasks(thread -> thread.getName().equals("customcrops-world-scheduler")); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void shutdownExecutor() { + this.worker.shutdown(); + try { + if (!this.worker.awaitTermination(1, TimeUnit.MINUTES)) { + this.plugin.getPluginLogger().severe("Timed out waiting for the CustomCrops worker thread pool to terminate"); + reportRunningTasks(thread -> thread.getName().startsWith("customcrops-world-worker-")); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void reportRunningTasks(Predicate predicate) { + Thread.getAllStackTraces().forEach((thread, stack) -> { + if (predicate.test(thread)) { + this.plugin.getPluginLogger().warn("Thread " + thread.getName() + " is blocked, and may be the reason for the slow shutdown!\n" + + Arrays.stream(stack).map(el -> " " + el).collect(Collectors.joining("\n")) + ); + } + }); + } + + private static final class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory { + private static final AtomicInteger COUNT = new AtomicInteger(0); + + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + thread.setDaemon(true); + thread.setName("customcrops-world-worker-" + COUNT.getAndIncrement()); + return thread; + } + } + + private final class ExceptionHandler implements UncaughtExceptionHandler { + @Override + public void uncaughtException(Thread t, Throwable e) { + WorldScheduler.this.plugin.getPluginLogger().warn("Thread " + t.getName() + " threw an uncaught exception", e); + } + } + + public static class JavaCancellable implements SchedulerTask { + + private final ScheduledFuture future; + + public JavaCancellable(ScheduledFuture future) { + this.future = future; + } + + @Override + public void cancel() { + this.future.cancel(false); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + } +} diff --git a/api/src/main/java/net/momirealms/customcrops/api/event/CropBreakEvent.java b/api/src/main/java/net/momirealms/customcrops/api/event/CropBreakEvent.java index c068aa46..3fae114a 100644 --- a/api/src/main/java/net/momirealms/customcrops/api/event/CropBreakEvent.java +++ b/api/src/main/java/net/momirealms/customcrops/api/event/CropBreakEvent.java @@ -17,9 +17,7 @@ package net.momirealms.customcrops.api.event; -import net.momirealms.customcrops.api.core.BuiltInBlockMechanics; import net.momirealms.customcrops.api.core.block.BreakReason; -import net.momirealms.customcrops.api.core.block.CropBlock; import net.momirealms.customcrops.api.core.mechanic.crop.CropConfig; import net.momirealms.customcrops.api.core.world.CustomCropsBlockState; import org.bukkit.Location; diff --git a/api/src/main/java/net/momirealms/customcrops/api/util/TagUtils.java b/api/src/main/java/net/momirealms/customcrops/api/util/TagUtils.java index 6df28942..55d6a804 100644 --- a/api/src/main/java/net/momirealms/customcrops/api/util/TagUtils.java +++ b/api/src/main/java/net/momirealms/customcrops/api/util/TagUtils.java @@ -17,7 +17,10 @@ package net.momirealms.customcrops.api.util; -import com.flowpowered.nbt.*; +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.Tag; +import com.flowpowered.nbt.TagType; import com.flowpowered.nbt.stream.NBTInputStream; import com.flowpowered.nbt.stream.NBTOutputStream; diff --git a/compatibility-itemsadder-r1/src/main/java/net/momirealms/customcrops/bukkit/integration/custom/itemsadder_r1/ItemsAdderListener.java b/compatibility-itemsadder-r1/src/main/java/net/momirealms/customcrops/bukkit/integration/custom/itemsadder_r1/ItemsAdderListener.java index 435e0476..822325fa 100644 --- a/compatibility-itemsadder-r1/src/main/java/net/momirealms/customcrops/bukkit/integration/custom/itemsadder_r1/ItemsAdderListener.java +++ b/compatibility-itemsadder-r1/src/main/java/net/momirealms/customcrops/bukkit/integration/custom/itemsadder_r1/ItemsAdderListener.java @@ -65,7 +65,8 @@ public void onInteractCustomBlock(CustomBlockInteractEvent event) { itemManager.handlePlayerInteractBlock( event.getPlayer(), event.getBlockClicked(), - event.getNamespacedID(), event.getBlockFace(), + event.getNamespacedID(), + event.getBlockFace(), event.getHand(), event.getItem(), event diff --git a/compatibility/src/main/java/net/momirealms/customcrops/bukkit/integration/worldedit/WorldEditListener.java b/compatibility/src/main/java/net/momirealms/customcrops/bukkit/integration/worldedit/WorldEditListener.java index 4afff315..d07e4533 100644 --- a/compatibility/src/main/java/net/momirealms/customcrops/bukkit/integration/worldedit/WorldEditListener.java +++ b/compatibility/src/main/java/net/momirealms/customcrops/bukkit/integration/worldedit/WorldEditListener.java @@ -39,7 +39,6 @@ import net.momirealms.customcrops.api.core.world.CustomCropsBlockState; import net.momirealms.customcrops.api.core.world.CustomCropsWorld; import net.momirealms.customcrops.api.core.world.Pos3; -import net.momirealms.customcrops.api.util.PluginUtils; import net.momirealms.customcrops.common.plugin.feature.Reloadable; import org.bukkit.Bukkit; import org.bukkit.entity.Player; diff --git a/gradle.properties b/gradle.properties index 9b7ff6d7..30b469d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Project settings # Rule: [major update].[feature update].[bug fix] -project_version=3.6.7 +project_version=3.6.8 config_version=39 project_group=net.momirealms @@ -18,10 +18,10 @@ adventure_platform_version=4.3.4 sparrow_heart_version=0.43 cloud_core_version=2.0.0 cloud_services_version=2.0.0 -cloud_brigadier_version=2.0.0-beta.9 -cloud_bukkit_version=2.0.0-beta.9 -cloud_paper_version=2.0.0-beta.9 -cloud_minecraft_extras_version=2.0.0-beta.9 +cloud_brigadier_version=2.0.0-beta.10 +cloud_bukkit_version=2.0.0-beta.10 +cloud_paper_version=2.0.0-beta.10 +cloud_minecraft_extras_version=2.0.0-beta.10 boosted_yaml_version=1.3.7 byte_buddy_version=1.14.18 mojang_brigadier_version=1.0.18 diff --git a/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/DebugWorldsCommand.java b/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/DebugWorldsCommand.java index 8ea00fc2..14a8a845 100644 --- a/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/DebugWorldsCommand.java +++ b/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/DebugWorldsCommand.java @@ -42,6 +42,7 @@ public Command.Builder assembleCommand(CommandManager { sender.sendMessage(AdventureHelper.miniMessage("World: " + world.getName() + "")); + sender.sendMessage(AdventureHelper.miniMessage(" - Loaded regions: " + w.loadedRegions().length)); sender.sendMessage(AdventureHelper.miniMessage(" - Loaded chunks: " + w.loadedChunks().length)); sender.sendMessage(AdventureHelper.miniMessage(" - Lazy chunks: " + w.lazyChunks().length)); }); diff --git a/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/ForceTickCommand.java b/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/ForceTickCommand.java index af658e83..2225c8ac 100644 --- a/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/ForceTickCommand.java +++ b/plugin/src/main/java/net/momirealms/customcrops/bukkit/command/feature/ForceTickCommand.java @@ -87,7 +87,7 @@ public Command.Builder assembleCommand(CommandManager customCropsWorld = optionalWorld.get(); - BukkitCustomCropsPlugin.getInstance().getScheduler().async().execute(() -> { + customCropsWorld.scheduler().async().execute(() -> { int amount = 0; long time1 = System.currentTimeMillis(); for (CustomCropsChunk customCropsChunk : customCropsWorld.loadedChunks()) { diff --git a/plugin/src/main/java/net/momirealms/customcrops/bukkit/integration/adaptor/BukkitWorldAdaptor.java b/plugin/src/main/java/net/momirealms/customcrops/bukkit/integration/adaptor/BukkitWorldAdaptor.java index 82501acb..fc5ab1f6 100644 --- a/plugin/src/main/java/net/momirealms/customcrops/bukkit/integration/adaptor/BukkitWorldAdaptor.java +++ b/plugin/src/main/java/net/momirealms/customcrops/bukkit/integration/adaptor/BukkitWorldAdaptor.java @@ -184,6 +184,12 @@ public CustomCropsChunk loadChunk(CustomCropsWorld world, ChunkPos pos, b @Override public void saveRegion(CustomCropsWorld world, CustomCropsRegion region) { File file = getRegionDataFile(world.world(), region.regionPos()); + if (region.canPrune()) { + if (file.exists()) { + file.delete(); + } + return; + } long time1 = System.currentTimeMillis(); File parentDir = file.getParentFile(); if (parentDir != null && !parentDir.exists()) { diff --git a/plugin/src/main/java/net/momirealms/customcrops/bukkit/world/BukkitWorldManager.java b/plugin/src/main/java/net/momirealms/customcrops/bukkit/world/BukkitWorldManager.java index bfe27982..ee0ee5f0 100644 --- a/plugin/src/main/java/net/momirealms/customcrops/bukkit/world/BukkitWorldManager.java +++ b/plugin/src/main/java/net/momirealms/customcrops/bukkit/world/BukkitWorldManager.java @@ -209,13 +209,11 @@ public void loadLoadedChunk(CustomCropsWorld world, ChunkPos pos) { if (world.isChunkLoaded(pos)) return; Optional customChunk = world.getChunk(pos); // don't load bukkit chunk again since it has been loaded - customChunk.ifPresent(customCropsChunk -> { - customCropsChunk.load(false); - }); + customChunk.ifPresent(customCropsChunk -> customCropsChunk.load(false)); } public void notifyOfflineUpdates(CustomCropsWorld world, ChunkPos pos) { - world.getChunk(pos).ifPresent(CustomCropsChunk::notifyOfflineTask); + world.getLoadedChunk(pos).ifPresent(CustomCropsChunk::notifyOfflineTask); } @Override @@ -225,14 +223,16 @@ public boolean unloadWorld(World world) { return false; } removedWorld.setTicking(false); - removedWorld.save(); + removedWorld.save(false); + removedWorld.scheduler().shutdownScheduler(); + removedWorld.scheduler().shutdownExecutor(); return true; } @EventHandler public void onWorldSave(WorldSaveEvent event) { final World world = event.getWorld(); - getWorld(world).ifPresent(CustomCropsWorld::save); + getWorld(world).ifPresent(world1 -> world1.save(true)); } @EventHandler (priority = EventPriority.HIGH)