From 3eb465dcc8560304e7f33691e42b870232958074 Mon Sep 17 00:00:00 2001 From: BestZige Date: Thu, 7 May 2026 16:06:53 +0700 Subject: [PATCH 1/7] feat: folia support --- pom.xml | 14 +++++++++++ .../espi/protectionstones/BlockHandler.java | 4 +-- .../espi/protectionstones/ListenerClass.java | 6 ++--- .../dev/espi/protectionstones/PSEconomy.java | 25 ++++++++++--------- .../protectionstones/ProtectionStones.java | 17 +++++++++++-- .../commands/ArgAddRemove.java | 4 +-- .../commands/ArgAdminCleanup.java | 10 ++++---- .../commands/ArgAdminHide.java | 6 ++--- .../commands/ArgAdminSetTaxAutopayers.java | 3 +-- .../protectionstones/commands/ArgCount.java | 3 +-- .../protectionstones/commands/ArgHome.java | 5 ++-- .../protectionstones/commands/ArgList.java | 2 +- .../protectionstones/commands/ArgMerge.java | 4 +-- .../protectionstones/commands/ArgTax.java | 3 +-- .../espi/protectionstones/commands/ArgTp.java | 14 +++++------ .../protectionstones/commands/ArgView.java | 4 +-- .../protectionstones/utils/ParticlesUtil.java | 3 +-- .../espi/protectionstones/utils/WGMerge.java | 3 +-- src/main/resources/plugin.yml | 9 ++++--- 19 files changed, 81 insertions(+), 58 deletions(-) diff --git a/pom.xml b/pom.xml index 3c7a417c..79c89a82 100644 --- a/pom.xml +++ b/pom.xml @@ -115,6 +115,10 @@ org.bstats dev.espi.protectionstones + + com.github.Anon8281.universalScheduler + dev.espi.protectionstones.universalScheduler + @@ -195,6 +199,10 @@ papermc https://papermc.io/repo/repository/maven-public/ + + jitpack.io + https://jitpack.io + @@ -204,6 +212,12 @@ 3.0.2 compile + + com.github.Anon8281 + UniversalScheduler + 0.1.7 + compile + org.spigotmc spigot-api diff --git a/src/main/java/dev/espi/protectionstones/BlockHandler.java b/src/main/java/dev/espi/protectionstones/BlockHandler.java index 891a7650..6989cc8e 100644 --- a/src/main/java/dev/espi/protectionstones/BlockHandler.java +++ b/src/main/java/dev/espi/protectionstones/BlockHandler.java @@ -275,7 +275,7 @@ public static boolean createActualRegion(Player p, Location l, PSProtectBlock bl if (blockOptions.autoHide) { PSL.msg(p, PSL.REGION_HIDDEN.msg()); // run on next tick so placing tile entities don't complain - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> l.getBlock().setType(Material.AIR)); + ProtectionStones.getScheduler().runTask(l, () -> l.getBlock().setType(Material.AIR)); } if (blockOptions.startWithTaxAutopay) { @@ -312,7 +312,7 @@ private static void playerMergeTask(Player p, PSRegion r) { // actually do auto merge if (!showGUI) { PSRegion finalMergeTo = mergeTo; - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { try { WGMerge.mergeRealRegions(p.getWorld(), r.getWGRegionManager(), finalMergeTo, Arrays.asList(finalMergeTo, r)); PSL.msg(p, PSL.MERGE_AUTO_MERGED.msg().replace("%region%", finalMergeTo.getId())); diff --git a/src/main/java/dev/espi/protectionstones/ListenerClass.java b/src/main/java/dev/espi/protectionstones/ListenerClass.java index 87175a00..9e2df3b3 100644 --- a/src/main/java/dev/espi/protectionstones/ListenerClass.java +++ b/src/main/java/dev/espi/protectionstones/ListenerClass.java @@ -67,7 +67,7 @@ public void onPlayerJoin(PlayerJoinEvent e) { UUIDCache.storeUUIDNamePair(p.getUniqueId(), p.getName()); // allow worldguard to resolve all UUIDs to names - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> UUIDCache.storeWGProfile(p.getUniqueId(), p.getName())); + ProtectionStones.getScheduler().runTaskAsynchronously(() -> UUIDCache.storeWGProfile(p.getUniqueId(), p.getName())); // add recipes to player's recipe book p.discoverRecipes(RecipeUtil.getRecipeKeys()); @@ -81,7 +81,7 @@ public void onPlayerJoin(PlayerJoinEvent e) { // tax join message if (ProtectionStones.getInstance().getConfigOptions().taxEnabled && ProtectionStones.getInstance().getConfigOptions().taxMessageOnJoin) { - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { int amount = 0; for (PSRegion psr : psp.getTaxEligibleRegions()) { for (PSRegion.TaxPayment tp : psr.getTaxPaymentsDue()) { @@ -576,7 +576,7 @@ public void onPSCreate(PSCreateEvent event) { if (!event.getRegion().getTypeOptions().eventsEnabled) return; // run on next tick (after the region is created to allow for edits to the region) - Bukkit.getServer().getScheduler().runTask(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTask(event.getRegion().getProtectBlock().getLocation(), () -> { // run custom commands (in config) for (String action : event.getRegion().getTypeOptions().regionCreateCommands) { execEvent(action, event.getPlayer(), event.getPlayer().getName(), event.getRegion()); diff --git a/src/main/java/dev/espi/protectionstones/PSEconomy.java b/src/main/java/dev/espi/protectionstones/PSEconomy.java index f776e00f..a11ccdbe 100644 --- a/src/main/java/dev/espi/protectionstones/PSEconomy.java +++ b/src/main/java/dev/espi/protectionstones/PSEconomy.java @@ -15,6 +15,7 @@ package dev.espi.protectionstones; +import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; import com.sk89q.worldguard.protection.managers.RegionManager; import com.sk89q.worldguard.protection.managers.storage.StorageException; import com.sk89q.worldguard.protection.regions.ProtectedRegion; @@ -38,7 +39,7 @@ public class PSEconomy { private List rentedList = new CopyOnWriteArrayList<>(); - private static int rentRunner = -1, taxRunner = -1; + private static MyScheduledTask rentRunner, taxRunner; public PSEconomy() { if (!ProtectionStones.getInstance().isVaultSupportEnabled()) { @@ -49,11 +50,11 @@ public PSEconomy() { loadRentList(); // start rent - rentRunner = Bukkit.getScheduler().runTaskTimerAsynchronously(ProtectionStones.getInstance(), this::updateRents, 0, 200).getTaskId(); + rentRunner = ProtectionStones.getScheduler().runTaskTimerAsynchronously(this::updateRents, 0, 200); // start taxes if (ProtectionStones.getInstance().getConfigOptions().taxEnabled) - taxRunner = Bukkit.getScheduler().runTaskTimerAsynchronously(ProtectionStones.getInstance(), this::updateTaxes, 0, 200).getTaskId(); + taxRunner = ProtectionStones.getScheduler().runTaskTimerAsynchronously(this::updateTaxes, 0, 200); } private synchronized void updateRents() { @@ -89,13 +90,13 @@ private void updateTaxes() { * Stops the economy cycle. Used for reloads when creating a new PSEconomy. */ public void stop() { - if (rentRunner != -1) { - Bukkit.getScheduler().cancelTask(rentRunner); - rentRunner = -1; + if (rentRunner != null) { + rentRunner.cancel(); + rentRunner = null; } - if (taxRunner != -1) { - Bukkit.getScheduler().cancelTask(taxRunner); - taxRunner = -1; + if (taxRunner != null) { + taxRunner.cancel(); + taxRunner = null; } } @@ -126,7 +127,7 @@ public void loadRentList() { public static void processTaxes(PSRegion r) { // if taxes are enabled for this regions if (r.getTypeOptions() != null && r.getTypeOptions().taxPeriod != -1) { - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTask(r.getProtectBlock().getLocation(), () -> { // update tax payments due r.updateTaxPayments(); @@ -191,7 +192,7 @@ public static void doRentPayment(PSRegion r) { } // update money must be run in main thread - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> tenant.pay(landlord, r.getPrice())); + ProtectionStones.getScheduler().runTask(() -> tenant.pay(landlord, r.getPrice())); r.setRentLastPaid(Instant.now().getEpochSecond()); try { // must save region to persist last paid r.getWGRegionManager().saveChanges(); @@ -208,4 +209,4 @@ public static void doRentPayment(PSRegion r) { public List getRentedList() { return rentedList; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/espi/protectionstones/ProtectionStones.java b/src/main/java/dev/espi/protectionstones/ProtectionStones.java index bd1775f9..0b650033 100644 --- a/src/main/java/dev/espi/protectionstones/ProtectionStones.java +++ b/src/main/java/dev/espi/protectionstones/ProtectionStones.java @@ -18,6 +18,8 @@ import com.electronwill.nightconfig.core.Config; import com.electronwill.nightconfig.core.file.CommentedFileConfig; import com.electronwill.nightconfig.toml.TomlFormat; +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import com.github.Anon8281.universalScheduler.scheduling.schedulers.TaskScheduler; import com.sk89q.worldguard.protection.managers.RegionManager; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import dev.espi.protectionstones.commands.ArgHelp; @@ -69,6 +71,7 @@ public class ProtectionStones extends JavaPlugin { private static List commandArgs = new ArrayList<>(); private static ProtectionStones plugin; + private static TaskScheduler scheduler; private PSEconomy economy; @@ -209,6 +212,10 @@ public static ProtectionStones getInstance() { return plugin; } + public static TaskScheduler getScheduler() { + return scheduler; + } + /** * @return the plugin's logger */ @@ -555,6 +562,7 @@ public void onEnable() { Config.setInsertionOrderPreserved(true); // make sure that config upgrades aren't a complete mess plugin = this; + scheduler = UniversalScheduler.getScheduler(this); configLocation = new File(this.getDataFolder() + "/config.toml"); blockDataFolder = new File(this.getDataFolder() + "/blocks"); @@ -637,7 +645,7 @@ public void onEnable() { // uuid cache getLogger().info("Building UUID cache... (if slow change async-load-uuid-cache in the config to true)"); if (configOptions.asyncLoadUUIDCache) { // async load - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + getScheduler().runTaskAsynchronously(() -> { for (OfflinePlayer op : Bukkit.getOfflinePlayers()) { UUIDCache.storeUUIDNamePair(op.getUniqueId(), op.getName()); } @@ -661,4 +669,9 @@ public void onEnable() { getLogger().info(ChatColor.WHITE + "ProtectionStones has successfully started!"); } -} \ No newline at end of file + @Override + public void onDisable() { + if (scheduler != null) scheduler.cancelTasks(); + } + +} diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java b/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java index b6f167e7..211b677e 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgAddRemove.java @@ -78,7 +78,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { List regions; // obtain region list that player is being added to or removed from @@ -129,7 +129,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap UUIDCache.storeWGProfile(addPlayerUuid, addPlayerName)); + ProtectionStones.getScheduler().runTaskAsynchronously(() -> UUIDCache.storeWGProfile(addPlayerUuid, addPlayerName)); } else if ((operationType.equals("remove") && r.isMember(addPlayerUuid)) || (operationType.equals("removeowner") && r.isOwner(addPlayerUuid))) { diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgAdminCleanup.java b/src/main/java/dev/espi/protectionstones/commands/ArgAdminCleanup.java index 4736246a..e1939000 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgAdminCleanup.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgAdminCleanup.java @@ -91,7 +91,7 @@ static boolean argumentAdminCleanup(CommandSender p, String[] preParseArgs) { // async cleanup task String finalAlias = alias; - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { int days = (args.size() > 0) ? Integer.parseInt(args.get(0)) : 30; // 30 days is default if days aren't specified PSL.msg(p, PSL.ADMIN_CLEANUP_HEADER.msg() @@ -146,8 +146,9 @@ static boolean argumentAdminCleanup(CommandSender p, String[] preParseArgs) { static private void regionLoop(Iterator deleteRegionsIterator, CommandSender p, boolean isRemoveOperation) { if (deleteRegionsIterator.hasNext()) { - Bukkit.getScheduler().runTaskLater(ProtectionStones.getInstance(), () -> - processRegion(deleteRegionsIterator, p, isRemoveOperation), 1); + PSRegion region = deleteRegionsIterator.next(); + ProtectionStones.getScheduler().runTaskLater(region.getProtectBlock().getLocation(), () -> + processRegion(region, deleteRegionsIterator, p, isRemoveOperation), 1); } else { // finished region iteration PSL.msg(p, PSL.ADMIN_CLEANUP_FOOTER.msg() .replace("%arg%", isRemoveOperation ? "remove" : "preview")); @@ -168,8 +169,7 @@ static private void regionLoop(Iterator deleteRegionsIterator, Command // Process a region, and then iterate to the next region on the next tick. // This is to prevent the server from pausing for the entire duration of the cleanup. // (lag from loading chunks to remove protection blocks) - static private void processRegion(Iterator deleteRegionsIterator, CommandSender p, boolean isRemoveOperation) { - PSRegion r = deleteRegionsIterator.next(); + static private void processRegion(PSRegion r, Iterator deleteRegionsIterator, CommandSender p, boolean isRemoveOperation) { if (isRemoveOperation) { // delete diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgAdminHide.java b/src/main/java/dev/espi/protectionstones/commands/ArgAdminHide.java index 30d1515e..c00a929f 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgAdminHide.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgAdminHide.java @@ -48,15 +48,15 @@ static boolean argumentAdminHide(CommandSender p, String[] args) { mgr = WGUtils.getRegionManagerWithWorld(w); } - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { // loop through regions that are protection stones and hide or unhide the block for (ProtectedRegion r : mgr.getRegions().values()) { if (ProtectionStones.isPSRegion(r)) { PSRegion region = PSRegion.fromWGRegion(w, r); if (args[1].equalsIgnoreCase("hide")) { - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), region::hide); + ProtectionStones.getScheduler().runTask(region.getProtectBlock().getLocation(), region::hide); } else if (args[1].equalsIgnoreCase("unhide")){ - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), region::unhide); + ProtectionStones.getScheduler().runTask(region.getProtectBlock().getLocation(), region::unhide); } } } diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgAdminSetTaxAutopayers.java b/src/main/java/dev/espi/protectionstones/commands/ArgAdminSetTaxAutopayers.java index 592e300e..a41e34ad 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgAdminSetTaxAutopayers.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgAdminSetTaxAutopayers.java @@ -22,7 +22,6 @@ import dev.espi.protectionstones.PSRegion; import dev.espi.protectionstones.ProtectionStones; import dev.espi.protectionstones.utils.WGUtils; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; @@ -34,7 +33,7 @@ static boolean argumentAdminSetTaxAutopayers(CommandSender s, String[] args) { PSL.msg(s, ChatColor.GRAY + "Scanning through regions, and setting tax autopayers for regions that don't have one..."); - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { WGUtils.getAllRegionManagers().forEach((w, rgm) -> { for (ProtectedRegion r : rgm.getRegions().values()) { PSRegion psr = PSRegion.fromWGRegion(w, r); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgCount.java b/src/main/java/dev/espi/protectionstones/commands/ArgCount.java index d72b5776..af79fde5 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgCount.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgCount.java @@ -20,7 +20,6 @@ import dev.espi.protectionstones.PSPlayer; import dev.espi.protectionstones.ProtectionStones; import dev.espi.protectionstones.utils.UUIDCache; -import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -68,7 +67,7 @@ public HashMap getRegisteredFlags() { @Override public boolean executeArgument(CommandSender s, String[] args, HashMap flags) { Player p = (Player) s; - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { int[] count; if (args.length == 1) { diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java index 44871a13..ab0b16a1 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java @@ -23,7 +23,6 @@ import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -80,7 +79,7 @@ public List tabComplete(CommandSender sender, String alias, String[] arg // cache home regions tabCache.put(p.getUniqueId(), regionNames); - Bukkit.getScheduler().runTaskLater(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskLater(() -> { tabCache.remove(p.getUniqueId()); }, 200); // remove cache after 10 seconds } @@ -128,7 +127,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { PSPlayer psp = PSPlayer.fromPlayer(p); if (args.length == 1) { // just "/ps home" diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgList.java b/src/main/java/dev/espi/protectionstones/commands/ArgList.java index 3b91f07d..5df815d7 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgList.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgList.java @@ -65,7 +65,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { if (args.length == 1) { List regions = psp.getPSRegionsCrossWorld(psp.getPlayer().getWorld(), true); display(s, regions, psp.getUuid(), true); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgMerge.java b/src/main/java/dev/espi/protectionstones/commands/ArgMerge.java index 35a79fc7..c06fe3c7 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgMerge.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgMerge.java @@ -129,7 +129,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { try { WGMerge.mergeRealRegions(p.getWorld(), rm, aRoot, Arrays.asList(aRegion, aRoot)); } catch (WGMerge.RegionHoleException e) { @@ -142,7 +142,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTask(p, () -> { if (!getGUI(p, PSRegion.fromWGRegion(p.getWorld(), rm.getRegion(aRoot.getId()))).isEmpty()) { Bukkit.dispatchCommand(p, ProtectionStones.getInstance().getConfigOptions().base_command + " merge"); } diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgTax.java b/src/main/java/dev/espi/protectionstones/commands/ArgTax.java index dd3b5eaf..8eeed368 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgTax.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgTax.java @@ -25,7 +25,6 @@ import net.md_5.bungee.api.chat.TextComponent; import net.milkbowl.vault.economy.EconomyResponse; import org.apache.commons.lang3.math.NumberUtils; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -104,7 +103,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap flags, PSPlayer p) { if (args.length == 2) { // /ps tax info - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { int pageNum = (flags.get("-p") == null || !MiscUtil.isValidInteger(flags.get("-p")) ? 0 : Integer.parseInt(flags.get("-p"))-1); List entries = new ArrayList<>(); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgTp.java b/src/main/java/dev/espi/protectionstones/commands/ArgTp.java index 5261ffcb..b63b85bb 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgTp.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgTp.java @@ -15,6 +15,7 @@ package dev.espi.protectionstones.commands; +import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; import dev.espi.protectionstones.*; import dev.espi.protectionstones.utils.ChatUtil; import dev.espi.protectionstones.utils.UUIDCache; @@ -23,14 +24,13 @@ import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitTask; import java.util.*; public class ArgTp implements PSCommandArg { private static HashMap waitCounter = new HashMap<>(); - private static HashMap taskCounter = new HashMap<>(); + private static HashMap taskCounter = new HashMap<>(); // /ps tp, /ps home @@ -66,7 +66,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { // get regions from the query List regions = ProtectionStones.getPSRegions(p.getWorld(), args[1]); @@ -100,7 +100,7 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { List regions = PSPlayer.fromUUID(tpUuid).getPSRegionsCrossWorld(p.getWorld(), false); // check if region was found @@ -138,12 +138,12 @@ static void teleportPlayer(Player p, PSRegion r) { if (r.getTypeOptions().tpWaitingSeconds == 0 || p.hasPermission("protectionstones.tp.bypasswait")) { // no teleport delay PSL.msg(p, PSL.TPING.msg()); - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> p.teleport(r.getHome())); // run on main thread, not async + ProtectionStones.getScheduler().runTask(p, () -> p.teleport(r.getHome())); // run on the owning entity thread, not async } else if (!r.getTypeOptions().noMovingWhenTeleportWaiting) { // teleport delay, but doesn't care about moving p.sendMessage(PSL.TP_IN_SECONDS.msg().replace("%seconds%", "" + r.getTypeOptions().tpWaitingSeconds)); - Bukkit.getScheduler().runTaskLater(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskLater(p, () -> { PSL.msg(p, PSL.TPING.msg()); p.teleport(r.getHome()); }, 20 * r.getTypeOptions().tpWaitingSeconds); @@ -158,7 +158,7 @@ static void teleportPlayer(Player p, PSRegion r) { // add teleport wait tasks to queue waitCounter.put(uuid, 0); - taskCounter.put(uuid, Bukkit.getScheduler().runTaskTimer(ProtectionStones.getInstance(), () -> { + taskCounter.put(uuid, ProtectionStones.getScheduler().runTaskTimer(p, () -> { Player pl = Bukkit.getPlayer(uuid); // cancel if the player is not on the server if (pl == null) { diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgView.java b/src/main/java/dev/espi/protectionstones/commands/ArgView.java index 4d5991f6..4bbc3143 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgView.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgView.java @@ -78,13 +78,13 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap cooldown.remove(p.getUniqueId()), 20 * ProtectionStones.getInstance().getConfigOptions().psViewCooldown); + ProtectionStones.getScheduler().runTaskLaterAsynchronously(() -> cooldown.remove(p.getUniqueId()), 20 * ProtectionStones.getInstance().getConfigOptions().psViewCooldown); int playerY = p.getLocation().getBlockY(), minY = r.getWGRegion().getMinimumPoint().getBlockY(), maxY = r.getWGRegion().getMaximumPoint().getBlockY(); // send particles to client - Bukkit.getScheduler().runTaskAsynchronously(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { AtomicInteger modU = new AtomicInteger(0); diff --git a/src/main/java/dev/espi/protectionstones/utils/ParticlesUtil.java b/src/main/java/dev/espi/protectionstones/utils/ParticlesUtil.java index 7bf96187..eef76bbd 100644 --- a/src/main/java/dev/espi/protectionstones/utils/ParticlesUtil.java +++ b/src/main/java/dev/espi/protectionstones/utils/ParticlesUtil.java @@ -16,7 +16,6 @@ package dev.espi.protectionstones.utils; import dev.espi.protectionstones.ProtectionStones; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.entity.Player; @@ -24,7 +23,7 @@ public class ParticlesUtil { public static void persistRedstoneParticle(Player p, Location l, Particle.DustOptions d, int occ) { for (int i = 0; i < occ; i++) { - Bukkit.getScheduler().runTaskLater(ProtectionStones.getInstance(), () -> { + ProtectionStones.getScheduler().runTaskLater(p, () -> { if (!p.isOnline()) return; // Stronger "glow marker" burst diff --git a/src/main/java/dev/espi/protectionstones/utils/WGMerge.java b/src/main/java/dev/espi/protectionstones/utils/WGMerge.java index 93cc60aa..dfa9f2b7 100644 --- a/src/main/java/dev/espi/protectionstones/utils/WGMerge.java +++ b/src/main/java/dev/espi/protectionstones/utils/WGMerge.java @@ -21,7 +21,6 @@ import com.sk89q.worldguard.protection.regions.ProtectedPolygonalRegion; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import dev.espi.protectionstones.*; -import org.bukkit.Bukkit; import org.bukkit.World; import java.util.*; @@ -262,7 +261,7 @@ public static PSRegion mergeRegions(String newID, World w, RegionManager rm, PSR for (PSRegion r : merge) { if (!r.getId().equals(newID)) { // run delete event for non-root real regions - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> r.deleteRegion(false)); + ProtectionStones.getScheduler().runTask(r.getProtectBlock().getLocation(), () -> r.deleteRegion(false)); } else { rm.removeRegion(r.getId()); } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d29b6003..903b8890 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -5,10 +5,11 @@ authors: [EspiDev] depend: [WorldGuard, WorldEdit] softdepend: [Vault, PlaceholderAPI, LuckPerms] -main: dev.espi.protectionstones.ProtectionStones -api-version: 1.21.10 - -permissions: +main: dev.espi.protectionstones.ProtectionStones +api-version: 1.21.10 +folia-supported: true + +permissions: protectionstones.create: description: Protect a region by placing a ProtectionStones block. default: op From 821b423692f41fe68bffc98d2b2ed483423c89d9 Mon Sep 17 00:00:00 2001 From: BestZige Date: Sat, 9 May 2026 14:05:15 +0700 Subject: [PATCH 2/7] fix: folia teleport --- .../protectionstones/commands/ArgHome.java | 19 +++--- .../espi/protectionstones/commands/ArgTp.java | 45 ++++++++----- .../protectionstones/utils/TeleportUtil.java | 64 +++++++++++++++++++ 3 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 src/main/java/dev/espi/protectionstones/utils/TeleportUtil.java diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java index ab0b16a1..dcbda8d9 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java @@ -24,6 +24,7 @@ import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.ChatColor; +import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.StringUtil; @@ -127,37 +128,39 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { - PSPlayer psp = PSPlayer.fromPlayer(p); + PSPlayer psp = PSPlayer.fromUUID(uuid); if (args.length == 1) { // just "/ps home" - List regions = psp.getHomes(p.getWorld()); + List regions = psp.getHomes(world); if (regions.size() == 1) { // teleport to home if there is only one home - ArgTp.teleportPlayer(p, regions.get(0)); + ArgTp.runForPlayer(p, () -> ArgTp.teleportPlayer(p, regions.get(0))); } else { // otherwise, open the GUI - openHomeGUI(psp, regions, (flags.get("-p") == null || !MiscUtil.isValidInteger(flags.get("-p")) ? 0 : Integer.parseInt(flags.get("-p")) - 1)); + ArgTp.runForPlayer(p, () -> openHomeGUI(PSPlayer.fromPlayer(p), regions, (flags.get("-p") == null || !MiscUtil.isValidInteger(flags.get("-p")) ? 0 : Integer.parseInt(flags.get("-p")) - 1))); } } else {// /ps home [id] // get regions from the query String query = args[1]; - List regions = psp.getHomes(p.getWorld()) + List regions = psp.getHomes(world) .stream() .filter(region -> region.getId().equals(query) || (region.getName() != null && region.getName().equals(query))) .collect(Collectors.toList()); if (regions.isEmpty()) { - PSL.msg(s, PSL.REGION_DOES_NOT_EXIST.msg()); + ArgTp.runForPlayer(p, () -> PSL.msg(s, PSL.REGION_DOES_NOT_EXIST.msg())); return; } // if there is more than one name in the query if (regions.size() > 1) { - ChatUtil.displayDuplicateRegionAliases(p, regions); + ArgTp.runForPlayer(p, () -> ChatUtil.displayDuplicateRegionAliases(p, regions)); return; } - ArgTp.teleportPlayer(p, regions.get(0)); + ArgTp.runForPlayer(p, () -> ArgTp.teleportPlayer(p, regions.get(0))); } }); diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgTp.java b/src/main/java/dev/espi/protectionstones/commands/ArgTp.java index b63b85bb..f9ecd5c5 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgTp.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgTp.java @@ -18,10 +18,12 @@ import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask; import dev.espi.protectionstones.*; import dev.espi.protectionstones.utils.ChatUtil; +import dev.espi.protectionstones.utils.TeleportUtil; import dev.espi.protectionstones.utils.UUIDCache; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -66,19 +68,20 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { // get regions from the query - List regions = ProtectionStones.getPSRegions(p.getWorld(), args[1]); + List regions = ProtectionStones.getPSRegions(world, args[1]); if (regions.isEmpty()) { - PSL.msg(s, PSL.REGION_DOES_NOT_EXIST.msg()); + runForPlayer(p, () -> PSL.msg(s, PSL.REGION_DOES_NOT_EXIST.msg())); return; } if (regions.size() > 1) { - ChatUtil.displayDuplicateRegionAliases(p, regions); + runForPlayer(p, () -> ChatUtil.displayDuplicateRegionAliases(p, regions)); return; } - teleportPlayer(p, regions.get(0)); + runForPlayer(p, () -> teleportPlayer(p, regions.get(0))); }); } else { // /ps tp [player] [num] // get the region id the player wants to teleport to @@ -100,22 +103,23 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { - List regions = PSPlayer.fromUUID(tpUuid).getPSRegionsCrossWorld(p.getWorld(), false); + List regions = PSPlayer.fromUUID(tpUuid).getPSRegionsCrossWorld(world, false); // check if region was found if (regions.isEmpty()) { - PSL.msg(p, PSL.REGION_NOT_FOUND_FOR_PLAYER.msg() - .replace("%player%", tpName)); + runForPlayer(p, () -> PSL.msg(p, PSL.REGION_NOT_FOUND_FOR_PLAYER.msg() + .replace("%player%", tpName))); return; } else if (regionNumber > regions.size()) { - PSL.msg(p, PSL.ONLY_HAS_REGIONS.msg() + runForPlayer(p, () -> PSL.msg(p, PSL.ONLY_HAS_REGIONS.msg() .replace("%player%", tpName) - .replace("%num%", "" + regions.size())); + .replace("%num%", "" + regions.size()))); return; } - teleportPlayer(p, regions.get(regionNumber - 1)); + runForPlayer(p, () -> teleportPlayer(p, regions.get(regionNumber - 1))); }); } @@ -128,6 +132,11 @@ public List tabComplete(CommandSender sender, String alias, String[] arg } static void teleportPlayer(Player p, PSRegion r) { + if (!ProtectionStones.getScheduler().isEntityThread(p)) { + runForPlayer(p, () -> teleportPlayer(p, r)); + return; + } + if (r.getTypeOptions() == null) { PSL.msg(p, ChatColor.RED + "This region is problematic, and the block type (" + r.getType() + ") is not configured. Please contact an administrator."); Bukkit.getLogger().info(ChatColor.RED + "This region is problematic, and the block type (" + r.getType() + ") is not configured."); @@ -138,14 +147,14 @@ static void teleportPlayer(Player p, PSRegion r) { if (r.getTypeOptions().tpWaitingSeconds == 0 || p.hasPermission("protectionstones.tp.bypasswait")) { // no teleport delay PSL.msg(p, PSL.TPING.msg()); - ProtectionStones.getScheduler().runTask(p, () -> p.teleport(r.getHome())); // run on the owning entity thread, not async + TeleportUtil.teleportAsync(p, r.getHome()); } else if (!r.getTypeOptions().noMovingWhenTeleportWaiting) { // teleport delay, but doesn't care about moving p.sendMessage(PSL.TP_IN_SECONDS.msg().replace("%seconds%", "" + r.getTypeOptions().tpWaitingSeconds)); ProtectionStones.getScheduler().runTaskLater(p, () -> { PSL.msg(p, PSL.TPING.msg()); - p.teleport(r.getHome()); + TeleportUtil.teleportAsync(p, r.getHome()); }, 20 * r.getTypeOptions().tpWaitingSeconds); } else {// delay and not allowed to move @@ -159,12 +168,12 @@ static void teleportPlayer(Player p, PSRegion r) { // add teleport wait tasks to queue waitCounter.put(uuid, 0); taskCounter.put(uuid, ProtectionStones.getScheduler().runTaskTimer(p, () -> { - Player pl = Bukkit.getPlayer(uuid); // cancel if the player is not on the server - if (pl == null) { + if (!p.isOnline()) { removeUUIDTimer(uuid); return; } + Player pl = p; if (waitCounter.get(uuid) == null) { removeUUIDTimer(uuid); @@ -183,7 +192,7 @@ static void teleportPlayer(Player p, PSRegion r) { } else if (waitCounter.get(uuid) == r.getTypeOptions().tpWaitingSeconds * 4) { // * 4 since this loops 4 times a second // if the timer has passed, teleport and cancel PSL.msg(pl, PSL.TPING.msg()); - pl.teleport(r.getHome()); + TeleportUtil.teleportAsync(pl, r.getHome()); removeUUIDTimer(uuid); } }, 5, 5) // loop 4 times a second @@ -200,4 +209,10 @@ private static void removeUUIDTimer(UUID uuid) { waitCounter.remove(uuid); taskCounter.remove(uuid); } + + static void runForPlayer(Player p, Runnable runnable) { + ProtectionStones.getScheduler().runTask(p, () -> { + if (p.isOnline()) runnable.run(); + }); + } } diff --git a/src/main/java/dev/espi/protectionstones/utils/TeleportUtil.java b/src/main/java/dev/espi/protectionstones/utils/TeleportUtil.java new file mode 100644 index 00000000..b257414c --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/utils/TeleportUtil.java @@ -0,0 +1,64 @@ +/* + * 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 dev.espi.protectionstones.utils; + +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import dev.espi.protectionstones.ProtectionStones; +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; + +public class TeleportUtil { + + public static CompletableFuture teleportAsync(Entity entity, Location location) { + if (entity == null || location == null) return CompletableFuture.completedFuture(false); + + Method teleportAsync = findTeleportAsyncMethod(entity); + if (teleportAsync != null) { + try { + Object result = teleportAsync.invoke(entity, location); + if (result instanceof CompletableFuture) { + return (CompletableFuture) result; + } + } catch (IllegalAccessException | InvocationTargetException e) { + ProtectionStones.getPluginLogger().warning("Unable to teleport entity asynchronously: " + e.getMessage()); + return CompletableFuture.completedFuture(false); + } + } + + if (UniversalScheduler.isFolia) { + ProtectionStones.getPluginLogger().warning("Folia is running, but teleportAsync(Location) is not available."); + return CompletableFuture.completedFuture(false); + } + + return CompletableFuture.completedFuture(entity.teleport(location)); + } + + private static Method findTeleportAsyncMethod(Entity entity) { + try { + return entity.getClass().getMethod("teleportAsync", Location.class); + } catch (NoSuchMethodException ignored) { + try { + return Entity.class.getMethod("teleportAsync", Location.class); + } catch (NoSuchMethodException ignoredAgain) { + return null; + } + } + } +} From 24877fcf37dd7949128206d3d4ec561a6b1d27af Mon Sep 17 00:00:00 2001 From: KarnZige Date: Sat, 23 May 2026 19:14:54 +0700 Subject: [PATCH 3/7] backup region --- .../espi/protectionstones/BlockHandler.java | 14 +++ .../protectionstones/utils/BackupUtil.java | 98 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/main/java/dev/espi/protectionstones/utils/BackupUtil.java diff --git a/src/main/java/dev/espi/protectionstones/BlockHandler.java b/src/main/java/dev/espi/protectionstones/BlockHandler.java index 6989cc8e..46cdebef 100644 --- a/src/main/java/dev/espi/protectionstones/BlockHandler.java +++ b/src/main/java/dev/espi/protectionstones/BlockHandler.java @@ -22,10 +22,12 @@ import com.sk89q.worldguard.protection.flags.Flags; import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.managers.storage.StorageException; import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; import com.sk89q.worldguard.protection.regions.ProtectedRegion; import dev.espi.protectionstones.commands.ArgMerge; import dev.espi.protectionstones.event.PSCreateEvent; +import dev.espi.protectionstones.utils.BackupUtil; import dev.espi.protectionstones.utils.LimitUtil; import dev.espi.protectionstones.utils.MiscUtil; import dev.espi.protectionstones.utils.WGMerge; @@ -289,6 +291,18 @@ public static boolean createActualRegion(Player p, Location l, PSProtectBlock bl if (r != null) playerMergeTask(p, r); } + // async backup: flush WG to disk then copy regions file — no main thread I/O + final RegionManager backupRm = rm; + final World backupWorld = l.getWorld(); + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { + try { + backupRm.saveChanges(); + } catch (StorageException e) { + e.printStackTrace(); + } + BackupUtil.backupWorldRegions(backupWorld); + }); + return true; } diff --git a/src/main/java/dev/espi/protectionstones/utils/BackupUtil.java b/src/main/java/dev/espi/protectionstones/utils/BackupUtil.java new file mode 100644 index 00000000..02a7e906 --- /dev/null +++ b/src/main/java/dev/espi/protectionstones/utils/BackupUtil.java @@ -0,0 +1,98 @@ +/* + * 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 dev.espi.protectionstones.utils; + +import dev.espi.protectionstones.ProtectionStones; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +public class BackupUtil { + + private static final long COOLDOWN_MILLIS = 30 * 60 * 1000L; // 30 minutes per world + private static final int MAX_FILES = 48; // ~24 hours of history + + // tracks last backup timestamp per world name — ConcurrentHashMap for async safety + private static final ConcurrentHashMap lastBackupTime = new ConcurrentHashMap<>(); + + // called from async thread — rate-limited copy of WG regions file for the given world + public static void backupWorldRegions(World world) { + Plugin wgPlugin = Bukkit.getPluginManager().getPlugin("WorldGuard"); + if (wgPlugin == null) return; + + String worldName = world.getName(); + + // atomic cooldown check: only proceed if 30 min have elapsed since last backup + long now = System.currentTimeMillis(); + if (!tryAcquireBackup(worldName, now)) return; + + File wgRegionsFile = new File(wgPlugin.getDataFolder(), + "worlds" + File.separator + worldName + File.separator + "regions.yml"); + if (!wgRegionsFile.exists()) return; + + File backupDir = new File(ProtectionStones.getInstance().getDataFolder(), + "backup" + File.separator + worldName); + backupDir.mkdirs(); + + String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date(now)); + File dest = new File(backupDir, worldName + "_" + timestamp + ".yml"); + + try { + Files.copy(wgRegionsFile.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + ProtectionStones.getInstance().getLogger() + .warning("Failed to backup WorldGuard regions for world " + worldName + ": " + e.getMessage()); + return; + } + + pruneOldBackups(backupDir, worldName); + } + + // atomically check and claim the backup slot — returns true if this call should proceed + private static boolean tryAcquireBackup(String worldName, long now) { + boolean[] proceed = {false}; + lastBackupTime.compute(worldName, (k, last) -> { + if (last == null || (now - last) >= COOLDOWN_MILLIS) { + proceed[0] = true; + return now; + } + return last; + }); + return proceed[0]; + } + + // delete oldest backup files beyond MAX_FILES limit + private static void pruneOldBackups(File backupDir, String worldName) { + File[] files = backupDir.listFiles( + (dir, name) -> name.startsWith(worldName + "_") && name.endsWith(".yml")); + if (files == null || files.length <= MAX_FILES) return; + + Arrays.sort(files, Comparator.comparingLong(File::lastModified)); + for (int i = 0; i < files.length - MAX_FILES; i++) { + files[i].delete(); + } + } +} From 0271f1b09157d0dd23788f4e13cf7abcb74bd6f6 Mon Sep 17 00:00:00 2001 From: BestZige Date: Fri, 5 Jun 2026 17:22:23 +0700 Subject: [PATCH 4/7] chore: fix console give not workings if full inv --- .../protectionstones/commands/ArgGet.java | 2 +- .../protectionstones/commands/ArgGive.java | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgGet.java b/src/main/java/dev/espi/protectionstones/commands/ArgGet.java index c66a2832..ee33cdfe 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgGet.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgGet.java @@ -57,7 +57,7 @@ public HashMap getRegisteredFlags() { private boolean openGetGUI(Player p) { PSL.msg(p, PSL.GET_HEADER.msg()); for (PSProtectBlock b : ProtectionStones.getInstance().getConfiguredBlocks()) { - if ((!b.permission.equals("") && !p.hasPermission(b.permission)) || (b.preventPsGet && !p.hasPermission("protectionstones.admin"))) { + if ((!b.permission.isEmpty() && !p.hasPermission(b.permission)) || (b.preventPsGet && !p.hasPermission("protectionstones.admin"))) { continue; // no permission } diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgGive.java b/src/main/java/dev/espi/protectionstones/commands/ArgGive.java index 5c2e7251..02b82fc8 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgGive.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgGive.java @@ -66,23 +66,37 @@ public boolean executeArgument(CommandSender p, String[] args, HashMap= 4 && NumberUtils.isNumber(args[3])) item.setAmount(Integer.parseInt(args[3])); - if (!ps.getInventory().addItem(item).isEmpty()) { + if (!ProtectionStones.getScheduler().isEntityThread(ps)) { + ProtectionStones.getScheduler().runTask(ps, () -> giveItem(p, ps, args[1], item)); + return true; + } + + return giveItem(p, ps, args[1], item); + } + + private boolean giveItem(CommandSender sender, Player player, String blockAlias, ItemStack item) { + if (!player.isOnline()) + return PSL.msg(sender, PSL.PLAYER_NOT_FOUND.msg() + " (" + player.getName() + ")"); + + Map leftovers = player.getInventory().addItem(item.clone()); + if (!leftovers.isEmpty()) { if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { - PSL.msg(ps, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); - ps.getWorld().dropItem(ps.getLocation(), cp.createItem()); + PSL.msg(player, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); + for (ItemStack leftover : leftovers.values()) { + player.getWorld().dropItem(player.getLocation(), leftover); + } } else { - return PSL.msg(p, PSL.GIVE_NO_INVENTORY_ROOM.msg()); + return PSL.msg(sender, PSL.GIVE_NO_INVENTORY_ROOM.msg()); } } - return PSL.msg(p, PSL.GIVE_GIVEN.msg().replace("%block%", args[1]).replace("%player%", Bukkit.getPlayer(args[2]).getDisplayName())); + return PSL.msg(sender, PSL.GIVE_GIVEN.msg().replace("%block%", blockAlias).replace("%player%", player.getDisplayName())); } // tab completion From c32a9254df739a158e8ab4f72f74b4ce7a279745 Mon Sep 17 00:00:00 2001 From: KarnZige Date: Sat, 4 Jul 2026 18:47:06 +0700 Subject: [PATCH 5/7] fix: make /ps unclaim safe on Folia and prevent item duplication Root cause: - /ps unclaim returned the protection stone item before deleting the region. - On Folia/Canvas, deleting the protection block could fail when it touched an unloaded chunk or the wrong region thread. - When that happened, the player already received the item, but the WorldGuard region still existed, allowing repeated unclaim attempts to duplicate items. Changes: - Move item return to happen only after the region is successfully deleted. - Run the unclaim delete step on the Folia region scheduler for the protection stone location. - Add a region-exists check to prevent repeated/concurrent unclaim attempts from returning duplicate items. - Pre-check inventory space when item dropping is disabled, preserving the no-room behavior before deletion. - Catch delete failures so failed unclaims do not return items. Merged region handling: - Stop using deleteRegion(true) from /ps unclaim because merged regions may contain protection blocks in multiple Folia region threads. - Delete the WorldGuard/merged region data first with deleteRegion(false). - Snapshot all protection block locations before deletion. - Schedule each physical protection block removal on its own Folia region scheduler. - Only remove a block if it still matches the original protection block type. --- .../protectionstones/commands/ArgUnclaim.java | 168 +++++++++++++++--- 1 file changed, 148 insertions(+), 20 deletions(-) diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java index cbc633f6..ac65c516 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java @@ -16,12 +16,17 @@ package dev.espi.protectionstones.commands; import dev.espi.protectionstones.*; +import dev.espi.protectionstones.utils.BlockUtil; import dev.espi.protectionstones.utils.TextGUI; +import dev.espi.protectionstones.utils.WGUtils; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -149,11 +154,57 @@ private void displayPSRegions(CommandSender s, List regions, int page) } private boolean unclaimBlock(PSRegion r, Player p) { + List items = getReturnedItems(r); + List blocksToRemove = getProtectBlocksToRemove(r); + if (!items.isEmpty() + && !ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull + && !hasInventorySpace(p, items)) { + PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); + return true; + } + + PSLocation psl = WGUtils.parsePSRegionToLocation(r.getId()); + Location protectBlockLocation = new Location(r.getWorld(), psl.x, psl.y, psl.z); + ProtectionStones.getScheduler().runTask(protectBlockLocation, () -> unclaimBlockAtRegion(r, p, items, blocksToRemove)); + return true; + } + + private void unclaimBlockAtRegion(PSRegion r, Player p, List items, List blocksToRemove) { + if (!regionStillExists(r)) { + return; + } + + // remove region + // check if removing the region and firing region remove event blocked it + boolean deleted; + try { + deleted = r.deleteRegion(false, p); + } catch (RuntimeException e) { + ProtectionStones.getPluginLogger().warning("Failed to unclaim region " + r.getId() + ": " + e.getMessage()); + ProtectionStones.getScheduler().runTask(p, () -> PSL.msg(p, PSL.REGION_DOES_NOT_EXIST.msg())); + return; + } + + if (!deleted) { + if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { // side case if the removing creates a hole and those are prevented + ProtectionStones.getScheduler().runTask(p, () -> PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg())); + } + return; + } + + removeProtectBlocks(blocksToRemove); + + ProtectionStones.getScheduler().runTask(p, () -> { + giveReturnedItems(p, items); + PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); + }); + } + + private List getReturnedItems(PSRegion r) { + List items = new ArrayList<>(); PSProtectBlock cpb = r.getTypeOptions(); if (cpb != null && !cpb.noDrop) { // return protection stone - List items = new ArrayList<>(); - if (r instanceof PSGroupRegion) { for (PSRegion rp : ((PSGroupRegion) r).getMergedRegions()) { if (rp.getTypeOptions() != null) items.add(rp.getTypeOptions().createItem()); @@ -161,31 +212,108 @@ private boolean unclaimBlock(PSRegion r, Player p) { } else { items.add(cpb.createItem()); } + } + return items; + } - for (ItemStack item : items) { - if (!p.getInventory().addItem(item).isEmpty()) { - // method will return not empty if item couldn't be added - if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { - PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); - p.getWorld().dropItem(p.getLocation(), item); - } else { - PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); - return true; - } - } + private List getProtectBlocksToRemove(PSRegion r) { + List blocks = new ArrayList<>(); + if (r instanceof PSGroupRegion) { + for (PSMergedRegion mergedRegion : ((PSGroupRegion) r).getMergedRegions()) { + blocks.add(ProtectBlockSnapshot.fromRegion(mergedRegion)); } + } else { + blocks.add(ProtectBlockSnapshot.fromRegion(r)); } - // remove region - // check if removing the region and firing region remove event blocked it - if (!r.deleteRegion(true, p)) { - if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { // side case if the removing creates a hole and those are prevented - PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg()); + return blocks; + } + + private void removeProtectBlocks(List blocks) { + for (ProtectBlockSnapshot snapshot : blocks) { + ProtectionStones.getScheduler().runTask(snapshot.location, () -> { + Block block = snapshot.location.getBlock(); + if (snapshot.type.equals(BlockUtil.getProtectBlockType(block))) { + block.setType(Material.AIR); + } + }); + } + } + + private void giveReturnedItems(Player p, List items) { + for (ItemStack item : items) { + HashMap leftovers = p.getInventory().addItem(item); + if (!leftovers.isEmpty()) { + // method will return not empty if item couldn't be added + if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { + PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); + leftovers.values().forEach(leftover -> p.getWorld().dropItem(p.getLocation(), leftover)); + } else { + PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); + leftovers.values().forEach(leftover -> p.getWorld().dropItem(p.getLocation(), leftover)); + } } - return true; } + } - PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); + private boolean hasInventorySpace(Player p, List items) { + List simulatedContents = new ArrayList<>(); + for (ItemStack content : p.getInventory().getStorageContents()) { + simulatedContents.add(content == null ? null : content.clone()); + } + + for (ItemStack item : items) { + int remaining = item.getAmount(); + for (ItemStack content : simulatedContents) { + if (content == null || !content.isSimilar(item)) continue; + int canFit = Math.min(item.getMaxStackSize(), content.getMaxStackSize()) - content.getAmount(); + if (canFit <= 0) continue; + int moved = Math.min(canFit, remaining); + content.setAmount(content.getAmount() + moved); + remaining -= moved; + if (remaining <= 0) break; + } + + while (remaining > 0) { + int emptySlot = firstEmptySlot(simulatedContents); + if (emptySlot == -1) return false; + int moved = Math.min(item.getMaxStackSize(), remaining); + ItemStack copy = item.clone(); + copy.setAmount(moved); + simulatedContents.set(emptySlot, copy); + remaining -= moved; + } + } return true; } + + private int firstEmptySlot(List items) { + for (int i = 0; i < items.size(); i++) { + ItemStack item = items.get(i); + if (item == null || item.getType().isAir()) return i; + } + return -1; + } + + private boolean regionStillExists(PSRegion r) { + if (r instanceof PSMergedRegion) { + return ((PSMergedRegion) r).getGroupRegion().hasMergedRegion(r.getId()); + } + return r.getWGRegionManager().getRegion(r.getId()) != null; + } + + private static class ProtectBlockSnapshot { + private final Location location; + private final String type; + + private ProtectBlockSnapshot(Location location, String type) { + this.location = location; + this.type = type; + } + + private static ProtectBlockSnapshot fromRegion(PSRegion r) { + PSLocation psl = WGUtils.parsePSRegionToLocation(r.getId()); + return new ProtectBlockSnapshot(new Location(r.getWorld(), psl.x, psl.y, psl.z), r.getType()); + } + } } From 901c059a8048092107912b8b093237c6fab5680e Mon Sep 17 00:00:00 2001 From: BestZige Date: Sat, 4 Jul 2026 21:48:43 +0700 Subject: [PATCH 6/7] Revert "fix: make /ps unclaim safe on Folia and prevent item duplication" This reverts commit c32a9254df739a158e8ab4f72f74b4ce7a279745. --- .../protectionstones/commands/ArgUnclaim.java | 168 +++--------------- 1 file changed, 20 insertions(+), 148 deletions(-) diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java index ac65c516..cbc633f6 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java @@ -16,17 +16,12 @@ package dev.espi.protectionstones.commands; import dev.espi.protectionstones.*; -import dev.espi.protectionstones.utils.BlockUtil; import dev.espi.protectionstones.utils.TextGUI; -import dev.espi.protectionstones.utils.WGUtils; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -154,57 +149,11 @@ private void displayPSRegions(CommandSender s, List regions, int page) } private boolean unclaimBlock(PSRegion r, Player p) { - List items = getReturnedItems(r); - List blocksToRemove = getProtectBlocksToRemove(r); - if (!items.isEmpty() - && !ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull - && !hasInventorySpace(p, items)) { - PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); - return true; - } - - PSLocation psl = WGUtils.parsePSRegionToLocation(r.getId()); - Location protectBlockLocation = new Location(r.getWorld(), psl.x, psl.y, psl.z); - ProtectionStones.getScheduler().runTask(protectBlockLocation, () -> unclaimBlockAtRegion(r, p, items, blocksToRemove)); - return true; - } - - private void unclaimBlockAtRegion(PSRegion r, Player p, List items, List blocksToRemove) { - if (!regionStillExists(r)) { - return; - } - - // remove region - // check if removing the region and firing region remove event blocked it - boolean deleted; - try { - deleted = r.deleteRegion(false, p); - } catch (RuntimeException e) { - ProtectionStones.getPluginLogger().warning("Failed to unclaim region " + r.getId() + ": " + e.getMessage()); - ProtectionStones.getScheduler().runTask(p, () -> PSL.msg(p, PSL.REGION_DOES_NOT_EXIST.msg())); - return; - } - - if (!deleted) { - if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { // side case if the removing creates a hole and those are prevented - ProtectionStones.getScheduler().runTask(p, () -> PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg())); - } - return; - } - - removeProtectBlocks(blocksToRemove); - - ProtectionStones.getScheduler().runTask(p, () -> { - giveReturnedItems(p, items); - PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); - }); - } - - private List getReturnedItems(PSRegion r) { - List items = new ArrayList<>(); PSProtectBlock cpb = r.getTypeOptions(); if (cpb != null && !cpb.noDrop) { // return protection stone + List items = new ArrayList<>(); + if (r instanceof PSGroupRegion) { for (PSRegion rp : ((PSGroupRegion) r).getMergedRegions()) { if (rp.getTypeOptions() != null) items.add(rp.getTypeOptions().createItem()); @@ -212,108 +161,31 @@ private List getReturnedItems(PSRegion r) { } else { items.add(cpb.createItem()); } - } - return items; - } - private List getProtectBlocksToRemove(PSRegion r) { - List blocks = new ArrayList<>(); - if (r instanceof PSGroupRegion) { - for (PSMergedRegion mergedRegion : ((PSGroupRegion) r).getMergedRegions()) { - blocks.add(ProtectBlockSnapshot.fromRegion(mergedRegion)); - } - } else { - blocks.add(ProtectBlockSnapshot.fromRegion(r)); - } - return blocks; - } - - private void removeProtectBlocks(List blocks) { - for (ProtectBlockSnapshot snapshot : blocks) { - ProtectionStones.getScheduler().runTask(snapshot.location, () -> { - Block block = snapshot.location.getBlock(); - if (snapshot.type.equals(BlockUtil.getProtectBlockType(block))) { - block.setType(Material.AIR); - } - }); - } - } - - private void giveReturnedItems(Player p, List items) { - for (ItemStack item : items) { - HashMap leftovers = p.getInventory().addItem(item); - if (!leftovers.isEmpty()) { - // method will return not empty if item couldn't be added - if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { - PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); - leftovers.values().forEach(leftover -> p.getWorld().dropItem(p.getLocation(), leftover)); - } else { - PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); - leftovers.values().forEach(leftover -> p.getWorld().dropItem(p.getLocation(), leftover)); + for (ItemStack item : items) { + if (!p.getInventory().addItem(item).isEmpty()) { + // method will return not empty if item couldn't be added + if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { + PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); + p.getWorld().dropItem(p.getLocation(), item); + } else { + PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); + return true; + } } } } - } - - private boolean hasInventorySpace(Player p, List items) { - List simulatedContents = new ArrayList<>(); - for (ItemStack content : p.getInventory().getStorageContents()) { - simulatedContents.add(content == null ? null : content.clone()); - } - - for (ItemStack item : items) { - int remaining = item.getAmount(); - for (ItemStack content : simulatedContents) { - if (content == null || !content.isSimilar(item)) continue; - int canFit = Math.min(item.getMaxStackSize(), content.getMaxStackSize()) - content.getAmount(); - if (canFit <= 0) continue; - int moved = Math.min(canFit, remaining); - content.setAmount(content.getAmount() + moved); - remaining -= moved; - if (remaining <= 0) break; - } - - while (remaining > 0) { - int emptySlot = firstEmptySlot(simulatedContents); - if (emptySlot == -1) return false; - int moved = Math.min(item.getMaxStackSize(), remaining); - ItemStack copy = item.clone(); - copy.setAmount(moved); - simulatedContents.set(emptySlot, copy); - remaining -= moved; + // remove region + // check if removing the region and firing region remove event blocked it + if (!r.deleteRegion(true, p)) { + if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { // side case if the removing creates a hole and those are prevented + PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg()); } + return true; } - return true; - } - - private int firstEmptySlot(List items) { - for (int i = 0; i < items.size(); i++) { - ItemStack item = items.get(i); - if (item == null || item.getType().isAir()) return i; - } - return -1; - } - - private boolean regionStillExists(PSRegion r) { - if (r instanceof PSMergedRegion) { - return ((PSMergedRegion) r).getGroupRegion().hasMergedRegion(r.getId()); - } - return r.getWGRegionManager().getRegion(r.getId()) != null; - } - - private static class ProtectBlockSnapshot { - private final Location location; - private final String type; - - private ProtectBlockSnapshot(Location location, String type) { - this.location = location; - this.type = type; - } + PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); - private static ProtectBlockSnapshot fromRegion(PSRegion r) { - PSLocation psl = WGUtils.parsePSRegionToLocation(r.getId()); - return new ProtectBlockSnapshot(new Location(r.getWorld(), psl.x, psl.y, psl.z), r.getType()); - } + return true; } } From a00ac5f28197ad81b23c710c8406d4e075ebfbd2 Mon Sep 17 00:00:00 2001 From: BestZige Date: Sat, 4 Jul 2026 21:50:21 +0700 Subject: [PATCH 7/7] Reapply "fix: make /ps unclaim safe on Folia and prevent item duplication" This reverts commit 901c059a8048092107912b8b093237c6fab5680e. --- .../protectionstones/commands/ArgUnclaim.java | 168 +++++++++++++++--- 1 file changed, 148 insertions(+), 20 deletions(-) diff --git a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java index cbc633f6..ac65c516 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgUnclaim.java @@ -16,12 +16,17 @@ package dev.espi.protectionstones.commands; import dev.espi.protectionstones.*; +import dev.espi.protectionstones.utils.BlockUtil; import dev.espi.protectionstones.utils.TextGUI; +import dev.espi.protectionstones.utils.WGUtils; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -149,11 +154,57 @@ private void displayPSRegions(CommandSender s, List regions, int page) } private boolean unclaimBlock(PSRegion r, Player p) { + List items = getReturnedItems(r); + List blocksToRemove = getProtectBlocksToRemove(r); + if (!items.isEmpty() + && !ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull + && !hasInventorySpace(p, items)) { + PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); + return true; + } + + PSLocation psl = WGUtils.parsePSRegionToLocation(r.getId()); + Location protectBlockLocation = new Location(r.getWorld(), psl.x, psl.y, psl.z); + ProtectionStones.getScheduler().runTask(protectBlockLocation, () -> unclaimBlockAtRegion(r, p, items, blocksToRemove)); + return true; + } + + private void unclaimBlockAtRegion(PSRegion r, Player p, List items, List blocksToRemove) { + if (!regionStillExists(r)) { + return; + } + + // remove region + // check if removing the region and firing region remove event blocked it + boolean deleted; + try { + deleted = r.deleteRegion(false, p); + } catch (RuntimeException e) { + ProtectionStones.getPluginLogger().warning("Failed to unclaim region " + r.getId() + ": " + e.getMessage()); + ProtectionStones.getScheduler().runTask(p, () -> PSL.msg(p, PSL.REGION_DOES_NOT_EXIST.msg())); + return; + } + + if (!deleted) { + if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { // side case if the removing creates a hole and those are prevented + ProtectionStones.getScheduler().runTask(p, () -> PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg())); + } + return; + } + + removeProtectBlocks(blocksToRemove); + + ProtectionStones.getScheduler().runTask(p, () -> { + giveReturnedItems(p, items); + PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); + }); + } + + private List getReturnedItems(PSRegion r) { + List items = new ArrayList<>(); PSProtectBlock cpb = r.getTypeOptions(); if (cpb != null && !cpb.noDrop) { // return protection stone - List items = new ArrayList<>(); - if (r instanceof PSGroupRegion) { for (PSRegion rp : ((PSGroupRegion) r).getMergedRegions()) { if (rp.getTypeOptions() != null) items.add(rp.getTypeOptions().createItem()); @@ -161,31 +212,108 @@ private boolean unclaimBlock(PSRegion r, Player p) { } else { items.add(cpb.createItem()); } + } + return items; + } - for (ItemStack item : items) { - if (!p.getInventory().addItem(item).isEmpty()) { - // method will return not empty if item couldn't be added - if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { - PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); - p.getWorld().dropItem(p.getLocation(), item); - } else { - PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); - return true; - } - } + private List getProtectBlocksToRemove(PSRegion r) { + List blocks = new ArrayList<>(); + if (r instanceof PSGroupRegion) { + for (PSMergedRegion mergedRegion : ((PSGroupRegion) r).getMergedRegions()) { + blocks.add(ProtectBlockSnapshot.fromRegion(mergedRegion)); } + } else { + blocks.add(ProtectBlockSnapshot.fromRegion(r)); } - // remove region - // check if removing the region and firing region remove event blocked it - if (!r.deleteRegion(true, p)) { - if (!ProtectionStones.getInstance().getConfigOptions().allowMergingHoles) { // side case if the removing creates a hole and those are prevented - PSL.msg(p, PSL.DELETE_REGION_PREVENTED_NO_HOLES.msg()); + return blocks; + } + + private void removeProtectBlocks(List blocks) { + for (ProtectBlockSnapshot snapshot : blocks) { + ProtectionStones.getScheduler().runTask(snapshot.location, () -> { + Block block = snapshot.location.getBlock(); + if (snapshot.type.equals(BlockUtil.getProtectBlockType(block))) { + block.setType(Material.AIR); + } + }); + } + } + + private void giveReturnedItems(Player p, List items) { + for (ItemStack item : items) { + HashMap leftovers = p.getInventory().addItem(item); + if (!leftovers.isEmpty()) { + // method will return not empty if item couldn't be added + if (ProtectionStones.getInstance().getConfigOptions().dropItemWhenInventoryFull) { + PSL.msg(p, PSL.NO_ROOM_DROPPING_ON_FLOOR.msg()); + leftovers.values().forEach(leftover -> p.getWorld().dropItem(p.getLocation(), leftover)); + } else { + PSL.msg(p, PSL.NO_ROOM_IN_INVENTORY.msg()); + leftovers.values().forEach(leftover -> p.getWorld().dropItem(p.getLocation(), leftover)); + } } - return true; } + } - PSL.msg(p, PSL.NO_LONGER_PROTECTED.msg()); + private boolean hasInventorySpace(Player p, List items) { + List simulatedContents = new ArrayList<>(); + for (ItemStack content : p.getInventory().getStorageContents()) { + simulatedContents.add(content == null ? null : content.clone()); + } + + for (ItemStack item : items) { + int remaining = item.getAmount(); + for (ItemStack content : simulatedContents) { + if (content == null || !content.isSimilar(item)) continue; + int canFit = Math.min(item.getMaxStackSize(), content.getMaxStackSize()) - content.getAmount(); + if (canFit <= 0) continue; + int moved = Math.min(canFit, remaining); + content.setAmount(content.getAmount() + moved); + remaining -= moved; + if (remaining <= 0) break; + } + + while (remaining > 0) { + int emptySlot = firstEmptySlot(simulatedContents); + if (emptySlot == -1) return false; + int moved = Math.min(item.getMaxStackSize(), remaining); + ItemStack copy = item.clone(); + copy.setAmount(moved); + simulatedContents.set(emptySlot, copy); + remaining -= moved; + } + } return true; } + + private int firstEmptySlot(List items) { + for (int i = 0; i < items.size(); i++) { + ItemStack item = items.get(i); + if (item == null || item.getType().isAir()) return i; + } + return -1; + } + + private boolean regionStillExists(PSRegion r) { + if (r instanceof PSMergedRegion) { + return ((PSMergedRegion) r).getGroupRegion().hasMergedRegion(r.getId()); + } + return r.getWGRegionManager().getRegion(r.getId()) != null; + } + + private static class ProtectBlockSnapshot { + private final Location location; + private final String type; + + private ProtectBlockSnapshot(Location location, String type) { + this.location = location; + this.type = type; + } + + private static ProtectBlockSnapshot fromRegion(PSRegion r) { + PSLocation psl = WGUtils.parsePSRegionToLocation(r.getId()); + return new ProtectBlockSnapshot(new Location(r.getWorld(), psl.x, psl.y, psl.z), r.getType()); + } + } }