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