diff --git a/Bukkit/build.gradle b/Bukkit/build.gradle index 821174878..8e32904c5 100644 --- a/Bukkit/build.gradle +++ b/Bukkit/build.gradle @@ -28,7 +28,7 @@ dependencies { exclude(module: "bukkit") } - compile("io.papermc:paperlib:1.0.2") + compile("io.papermc:paperlib:1.0.4") implementation("net.kyori:text-adapter-bukkit:3.0.3") compile("com.github.MilkBowl:VaultAPI:1.7") { exclude(module: "bukkit") @@ -96,7 +96,7 @@ task copyFiles { shadowJar { dependencies { include(dependency(":PlotSquared-Core")) - include(dependency("io.papermc:paperlib:1.0.2")) + include(dependency("io.papermc:paperlib:1.0.4")) include(dependency("net.kyori:text-adapter-bukkit:3.0.3")) include(dependency("org.bstats:bstats-bukkit:1.7")) include(dependency("org.khelekore:prtree:1.7.0-SNAPSHOT")) diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java index 1027ea88d..e40b6698e 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java @@ -389,9 +389,11 @@ import static com.plotsquared.core.util.ReflectionUtils.getRefClass; logger.info("[P2] (UUID) Using the offline mode UUID service"); } - final OfflinePlayerUUIDService offlinePlayerUUIDService = new OfflinePlayerUUIDService(); - this.impromptuPipeline.registerService(offlinePlayerUUIDService); - this.backgroundPipeline.registerService(offlinePlayerUUIDService); + if (Settings.UUID.SERVICE_BUKKIT) { + final OfflinePlayerUUIDService offlinePlayerUUIDService = new OfflinePlayerUUIDService(); + this.impromptuPipeline.registerService(offlinePlayerUUIDService); + this.backgroundPipeline.registerService(offlinePlayerUUIDService); + } final SQLiteUUIDService sqLiteUUIDService = new SQLiteUUIDService("user_cache.db"); @@ -404,7 +406,8 @@ import static com.plotsquared.core.util.ReflectionUtils.getRefClass; } final LuckPermsUUIDService luckPermsUUIDService; - if (Bukkit.getPluginManager().getPlugin("LuckPerms") != null) { + if (Settings.UUID.SERVICE_LUCKPERMS && + Bukkit.getPluginManager().getPlugin("LuckPerms") != null) { luckPermsUUIDService = new LuckPermsUUIDService(); logger.info("[P2] (UUID) Using LuckPerms as a complementary UUID service"); } else { @@ -412,7 +415,8 @@ import static com.plotsquared.core.util.ReflectionUtils.getRefClass; } final BungeePermsUUIDService bungeePermsUUIDService; - if (Bukkit.getPluginManager().getPlugin("BungeePerms") != null) { + if (Settings.UUID.SERVICE_BUNGEE_PERMS && + Bukkit.getPluginManager().getPlugin("BungeePerms") != null) { bungeePermsUUIDService = new BungeePermsUUIDService(); logger.info("[P2] (UUID) Using BungeePerms as a complementary UUID service"); } else { @@ -420,16 +424,16 @@ import static com.plotsquared.core.util.ReflectionUtils.getRefClass; } final EssentialsUUIDService essentialsUUIDService; - if (Bukkit.getPluginManager().getPlugin("Essentials") != null) { + if (Settings.UUID.SERVICE_ESSENTIALSX && Bukkit.getPluginManager().getPlugin("Essentials") != null) { essentialsUUIDService = new EssentialsUUIDService(); - logger.info("[P2] (UUID) Using Essentials as a complementary UUID service"); + logger.info("[P2] (UUID) Using EssentialsX as a complementary UUID service"); } else { essentialsUUIDService = null; } if (!Settings.UUID.OFFLINE) { // If running Paper we'll also try to use their profiles - if (PaperLib.isPaper()) { + if (Bukkit.getOnlineMode() && PaperLib.isPaper() && Settings.UUID.SERVICE_PAPER) { final PaperUUIDService paperUUIDService = new PaperUUIDService(); this.impromptuPipeline.registerService(paperUUIDService); this.backgroundPipeline.registerService(paperUUIDService); diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/listener/PlayerEvents.java b/Bukkit/src/main/java/com/plotsquared/bukkit/listener/PlayerEvents.java index d218beac0..ce868a61e 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/listener/PlayerEvents.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/listener/PlayerEvents.java @@ -1535,8 +1535,9 @@ import java.util.regex.Pattern; } for (Block block1 : event.getBlocks()) { Location bloc = BukkitUtil.getLocation(block1.getLocation()); - if (bloc.isPlotArea() || bloc.add(relative.getBlockX(), - relative.getBlockY(), relative.getBlockZ()).isPlotArea()) { + if (bloc.isPlotArea() || bloc + .add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ()) + .isPlotArea()) { event.setCancelled(true); return; } @@ -1567,8 +1568,8 @@ import java.util.regex.Pattern; return; } } - if (!plot.equals(area.getOwnedPlot(location.add( - relative.getBlockX(), relative.getBlockY(), relative.getBlockZ())))) { + if (!plot.equals(area.getOwnedPlot( + location.add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ())))) { // This branch is only necessary to prevent pistons from extending // if they are: on a plot edge, facing outside the plot, and not // pushing any blocks @@ -1589,8 +1590,9 @@ import java.util.regex.Pattern; } for (Block block1 : event.getBlocks()) { Location bloc = BukkitUtil.getLocation(block1.getLocation()); - if (bloc.isPlotArea() || bloc.add(relative.getBlockX(), - relative.getBlockY(), relative.getBlockZ()).isPlotArea()) { + if (bloc.isPlotArea() || bloc + .add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ()) + .isPlotArea()) { event.setCancelled(true); return; } @@ -1976,7 +1978,7 @@ import java.util.regex.Pattern; } if (event.getAction() == Action.RIGHT_CLICK_AIR) { Material item = event.getMaterial(); - if (item.toString().toLowerCase().endsWith("egg")) { + if (item.toString().toLowerCase().endsWith("_egg")) { event.setCancelled(true); event.setUseItemInHand(Event.Result.DENY); } @@ -1988,7 +1990,7 @@ import java.util.regex.Pattern; if (type == Material.AIR) { type = offType; } - if (type.toString().toLowerCase().endsWith("egg")) { + if (type.toString().toLowerCase().endsWith("_egg")) { Block block = player.getTargetBlockExact(5, FluidCollisionMode.SOURCE_ONLY); if (block != null && block.getType() != Material.AIR) { Location location = BukkitUtil.getLocation(block.getLocation()); diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/queue/ChunkCoordinator.java b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/ChunkCoordinator.java new file mode 100644 index 000000000..cc5c1f72f --- /dev/null +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/queue/ChunkCoordinator.java @@ -0,0 +1,326 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2020 IntellectualSites + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.bukkit.queue; + +import com.google.common.base.Preconditions; +import com.plotsquared.bukkit.BukkitMain; +import com.sk89q.worldedit.math.BlockVector2; +import io.papermc.lib.PaperLib; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * Utility that allows for the loading and coordination of chunk actions + *

+ * The coordinator takes in collection of chunk coordinates, loads them + * and allows the caller to specify a sink for the loaded chunks. The + * coordinator will prevent the chunks from being unloaded until the sink + * has fully consumed the chunk + *

+ * Usage: + *

{@code
+ * final ChunkCoordinator chunkCoordinator = ChunkCoordinator.builder()
+ *     .inWorld(Objects.requireNonNull(Bukkit.getWorld("world"))).withChunk(BlockVector2.at(0, 0))
+ *     .withConsumer(chunk -> System.out.printf("Got chunk %d;%d", chunk.getX(), chunk.getZ()))
+ *     .withFinalAction(() -> System.out.println("All chunks have been loaded"))
+ *     .withThrowableConsumer(throwable -> System.err.println("Something went wrong... =("))
+ *     .withMaxIterationTime(25L)
+ *     .build();
+ * chunkCoordinator.subscribeToProgress((coordinator, progress) ->
+ *     System.out.printf("Progress: %.1f", progress * 100.0f));
+ * chunkCoordinator.start();
+ * }
+ * + * @author Alexander Söderberg + * @see #builder() To create a new coordinator instance + */ +public final class ChunkCoordinator extends BukkitRunnable { + + private final List progressSubscribers = new LinkedList<>(); + + private final Queue requestedChunks; + private final Queue availableChunks; + private final long maxIterationTime; + private final Plugin plugin; + private final Consumer chunkConsumer; + private final World world; + private final Runnable whenDone; + private final Consumer throwableConsumer; + private final int totalSize; + + private AtomicInteger expectedSize; + private int batchSize; + + private ChunkCoordinator(final long maxIterationTime, final int initialBatchSize, + @NotNull final Consumer chunkConsumer, @NotNull final World world, + @NotNull final Collection requestedChunks, @NotNull final Runnable whenDone, + @NotNull final Consumer throwableConsumer) { + this.requestedChunks = new LinkedBlockingQueue<>(requestedChunks); + this.availableChunks = new LinkedBlockingQueue<>(); + this.totalSize = requestedChunks.size(); + this.expectedSize = new AtomicInteger(this.totalSize); + this.world = world; + this.batchSize = initialBatchSize; + this.chunkConsumer = chunkConsumer; + this.maxIterationTime = maxIterationTime; + this.whenDone = whenDone; + this.throwableConsumer = throwableConsumer; + this.plugin = JavaPlugin.getPlugin(BukkitMain.class); + } + + /** + * Create a new {@link ChunkCoordinator} instance + * + * @return Coordinator builder instance + */ + @NotNull public static ChunkCoordinatorBuilder builder() { + return new ChunkCoordinatorBuilder(); + } + + /** + * Start the coordinator instance + */ + public void start() { + // Request initial batch + this.requestBatch(); + // Wait until next tick to give the chunks a chance to be loaded + this.runTaskTimer(this.plugin, 1L, 1L); + } + + @Override public void run() { + Chunk chunk = this.availableChunks.poll(); + if (chunk == null) { + return; + } + long iterationTime; + int processedChunks = 0; + do { + final long start = System.currentTimeMillis(); + try { + this.chunkConsumer.accept(chunk); + } catch (final Throwable throwable) { + this.throwableConsumer.accept(throwable); + } + this.freeChunk(chunk); + processedChunks++; + final long end = System.currentTimeMillis(); + // Update iteration time + iterationTime = end - start; + } while (2 * iterationTime /* last chunk + next chunk */ < this.maxIterationTime + && (chunk = availableChunks.poll()) != null); + if (processedChunks < this.batchSize) { + // Adjust batch size based on the amount of processed chunks per tick + this.batchSize = processedChunks; + } + + final int expected = this.expectedSize.addAndGet(-processedChunks); + + final float progress = ((float) totalSize - (float) expected) / (float) totalSize; + for (final ProgressSubscriber subscriber : this.progressSubscribers) { + subscriber.notifyProgress(this, progress); + } + + if (expected <= 0) { + try { + this.whenDone.run(); + } catch (final Throwable throwable) { + this.throwableConsumer.accept(throwable); + } + this.cancel(); + } else { + if (this.availableChunks.size() < processedChunks) { + this.requestBatch(); + } + } + } + + private void requestBatch() { + BlockVector2 chunk; + for (int i = 0; i < this.batchSize && (chunk = this.requestedChunks.poll()) != null; i++) { + // This required PaperLib to be bumped to version 1.0.4 to mark the request as urgent + PaperLib.getChunkAtAsync(this.world, chunk.getX(), chunk.getZ(), true, true) + .whenComplete((chunkObject, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + // We want one less because this couldn't be processed + this.expectedSize.decrementAndGet(); + } else { + this.processChunk(chunkObject); + } + }); + } + } + + private void processChunk(@NotNull final Chunk chunk) { + if (!chunk.isLoaded()) { + throw new IllegalArgumentException( + String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ())); + } + chunk.addPluginChunkTicket(this.plugin); + this.availableChunks.add(chunk); + } + + private void freeChunk(@NotNull final Chunk chunk) { + if (!chunk.isLoaded()) { + throw new IllegalArgumentException( + String.format("Chunk %d;%d is is not loaded", chunk.getX(), chunk.getZ())); + } + chunk.removePluginChunkTicket(this.plugin); + } + + /** + * Get the amount of remaining chunks (at the time of the method call) + * + * @return Snapshot view of remaining chunk count + */ + public int getRemainingChunks() { + return this.expectedSize.get(); + } + + /** + * Get the amount of requested chunks + * + * @return Requested chunk count + */ + public int getTotalChunks() { + return this.totalSize; + } + + /** + * Subscribe to coordinator progress updates + * + * @param subscriber Subscriber + */ + public void subscribeToProgress(@NotNull final ChunkCoordinator.ProgressSubscriber subscriber) { + this.progressSubscribers.add(subscriber); + } + + + @FunctionalInterface + public interface ProgressSubscriber { + + /** + * Notify about a progress update in the coordinator + * + * @param coordinator Coordinator instance that triggered the notification + * @param progress Progress in the range [0, 1] + */ + void notifyProgress(@NotNull final ChunkCoordinator coordinator, final float progress); + + } + + + public static final class ChunkCoordinatorBuilder { + + private final List requestedChunks = new LinkedList<>(); + private Consumer throwableConsumer = Throwable::printStackTrace; + private World world; + private Consumer chunkConsumer; + private Runnable whenDone = () -> { + }; + private long maxIterationTime = 60; // A little over 1 tick; + private int initialBatchSize = 4; + + private ChunkCoordinatorBuilder() { + } + + @NotNull public ChunkCoordinatorBuilder inWorld(@NotNull final World world) { + this.world = Preconditions.checkNotNull(world, "World may not be null"); + return this; + } + + @NotNull + public ChunkCoordinatorBuilder withChunk(@NotNull final BlockVector2 chunkLocation) { + this.requestedChunks + .add(Preconditions.checkNotNull(chunkLocation, "Chunk location may not be null")); + return this; + } + + @NotNull public ChunkCoordinatorBuilder withChunks( + @NotNull final Collection chunkLocations) { + chunkLocations.forEach(this::withChunk); + return this; + } + + @NotNull + public ChunkCoordinatorBuilder withConsumer(@NotNull final Consumer chunkConsumer) { + this.chunkConsumer = + Preconditions.checkNotNull(chunkConsumer, "Chunk consumer may not be null"); + return this; + } + + @NotNull public ChunkCoordinatorBuilder withFinalAction(@NotNull final Runnable whenDone) { + this.whenDone = Preconditions.checkNotNull(whenDone, "Final action may not be null"); + return this; + } + + @NotNull public ChunkCoordinatorBuilder withMaxIterationTime(final long maxIterationTime) { + Preconditions + .checkArgument(maxIterationTime > 0, "Max iteration time must be positive"); + this.maxIterationTime = maxIterationTime; + return this; + } + + @NotNull public ChunkCoordinatorBuilder withInitialBatchSize(final int initialBatchSize) { + Preconditions + .checkArgument(initialBatchSize > 0, "Initial batch size must be positive"); + this.initialBatchSize = initialBatchSize; + return this; + } + + @NotNull public ChunkCoordinatorBuilder withThrowableConsumer( + @NotNull final Consumer throwableConsumer) { + this.throwableConsumer = + Preconditions.checkNotNull(throwableConsumer, "Throwable consumer may not be null"); + return this; + } + + @NotNull public ChunkCoordinator build() { + Preconditions.checkNotNull(this.world, "No world was supplied"); + Preconditions.checkNotNull(this.chunkConsumer, "No chunk consumer was supplied"); + Preconditions.checkNotNull(this.whenDone, "No final action was supplied"); + Preconditions + .checkNotNull(this.throwableConsumer, "No throwable consumer was supplied"); + return new ChunkCoordinator(this.maxIterationTime, this.initialBatchSize, + this.chunkConsumer, this.world, this.requestedChunks, this.whenDone, + this.throwableConsumer); + } + + } + +} diff --git a/Core/src/main/java/com/plotsquared/core/command/Target.java b/Core/src/main/java/com/plotsquared/core/command/Target.java index 410aca635..6c1fdc6c6 100644 --- a/Core/src/main/java/com/plotsquared/core/command/Target.java +++ b/Core/src/main/java/com/plotsquared/core/command/Target.java @@ -42,7 +42,7 @@ import com.plotsquared.core.util.query.PlotQuery; public class Target extends SubCommand { public Target() { - super(Argument.PlotID); + super(); } @Override public boolean onCommand(PlotPlayer player, String[] args) { @@ -51,6 +51,10 @@ public class Target extends SubCommand { MainUtil.sendMessage(player, Captions.NOT_IN_PLOT_WORLD); return false; } + if (args.length == 0) { + MainUtil.sendMessage(player, this.getUsage()); + return false; + } Plot target = null; if (StringMan.isEqualIgnoreCaseToAny(args[0], "near", "nearest")) { int distance = Integer.MAX_VALUE; diff --git a/Core/src/main/java/com/plotsquared/core/configuration/Settings.java b/Core/src/main/java/com/plotsquared/core/configuration/Settings.java index f575fba43..e2fdfde60 100644 --- a/Core/src/main/java/com/plotsquared/core/configuration/Settings.java +++ b/Core/src/main/java/com/plotsquared/core/configuration/Settings.java @@ -260,6 +260,16 @@ public class Settings extends Config { @Comment("Whether or not automatic background caching should be enabled. It is HIGHLY recommended to keep this turned on." + " This should only be disabled if the server has a very large number of plots (>100k)") public static boolean BACKGROUND_CACHING_ENABLED = true; + @Comment("Whether the PaperMC service is enabled") + public static boolean SERVICE_PAPER = true; + @Comment("Whether the LuckPerms service is enabled") + public static boolean SERVICE_LUCKPERMS = true; + @Comment("Whether the Bukkit service is enabled") + public static boolean SERVICE_BUKKIT = true; + @Comment("Whether the EssentialsX service is enabled") + public static boolean SERVICE_ESSENTIALSX = true; + @Comment("Whether the BungeePerms service is enabled") + public static boolean SERVICE_BUNGEE_PERMS = true; }