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..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; @@ -275,7 +277,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) { @@ -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; } @@ -312,7 +326,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..dcbda8d9 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgHome.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgHome.java @@ -23,8 +23,8 @@ 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.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.StringUtil; @@ -80,7 +80,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,37 +128,39 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { - PSPlayer psp = PSPlayer.fromPlayer(p); + UUID uuid = p.getUniqueId(); + World world = p.getWorld(); + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { + 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/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..f9ecd5c5 100644 --- a/src/main/java/dev/espi/protectionstones/commands/ArgTp.java +++ b/src/main/java/dev/espi/protectionstones/commands/ArgTp.java @@ -15,22 +15,24 @@ 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.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; -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,19 +68,20 @@ public boolean executeArgument(CommandSender s, String[] args, HashMap { + World world = p.getWorld(); + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { // 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); + World world = p.getWorld(); + ProtectionStones.getScheduler().runTaskAsynchronously(() -> { + 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()); - Bukkit.getScheduler().runTask(ProtectionStones.getInstance(), () -> p.teleport(r.getHome())); // run on main 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)); - Bukkit.getScheduler().runTaskLater(ProtectionStones.getInstance(), () -> { + 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 @@ -158,13 +167,13 @@ 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(), () -> { - Player pl = Bukkit.getPlayer(uuid); + taskCounter.put(uuid, ProtectionStones.getScheduler().runTaskTimer(p, () -> { // 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/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/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(); + } + } +} 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/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; + } + } + } +} 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