From beb74f93dc28e597a1d8ff57d0efe44d9dbb0998 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:11:29 +0000 Subject: [PATCH 1/7] Initial plan From 56038ce4ea4336fb3f889cd64352c8d85a0aecd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:14:40 +0000 Subject: [PATCH 2/7] Initial exploration complete - creating implementation plan Co-authored-by: modpotato <81768237+modpotato@users.noreply.github.com> --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 40c36ec1ec8f16a395264a0d876d2a19c7ed1c56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:20:33 +0000 Subject: [PATCH 3/7] Implement thread-safe debris restoration, command improvements, bypass consistency, and Folia task hygiene Co-authored-by: modpotato <81768237+modpotato@users.noreply.github.com> --- .../commands/AntiNetheriteCommand.java | 155 ++++++------- .../modpotato/listeners/AttackListener.java | 5 + .../modpotato/listeners/CraftListener.java | 5 + .../top/modpotato/listeners/DropListener.java | 7 + .../modpotato/listeners/EquipListener.java | 24 ++ .../listeners/InventoryMoveListener.java | 10 + .../modpotato/listeners/MiningListener.java | 8 +- .../modpotato/listeners/PickupListener.java | 5 + .../modpotato/scheduler/NetheriteRemover.java | 73 +++--- .../top/modpotato/util/DebrisStorage.java | 216 ++++++++++++++---- 10 files changed, 351 insertions(+), 157 deletions(-) diff --git a/src/main/java/top/modpotato/commands/AntiNetheriteCommand.java b/src/main/java/top/modpotato/commands/AntiNetheriteCommand.java index 9d037cf..2154e5d 100644 --- a/src/main/java/top/modpotato/commands/AntiNetheriteCommand.java +++ b/src/main/java/top/modpotato/commands/AntiNetheriteCommand.java @@ -26,9 +26,8 @@ public class AntiNetheriteCommand implements CommandExecutor, TabCompleter { // Map of user-friendly setting names to actual config paths private final Map SETTINGS_MAP = new HashMap<>(); - // Track when the last restore command was used to prevent spam - private long lastRestoreTime = 0; - private static final long RESTORE_COOLDOWN = 10000; // 10 seconds cooldown + // Track when the last restore command was used per sender to prevent spam + private final Map lastRestoreTimes = new HashMap<>(); public AntiNetheriteCommand(Main plugin) { this.plugin = plugin; @@ -50,7 +49,7 @@ public AntiNetheriteCommand(Main plugin) { SETTINGS_MAP.put("remove-dropped", "anti-netherite.item-handling.remove-dropped"); // Ancient debris settings - SETTINGS_MAP.put("replace-ancient-debris", "anti-netherite.ancient-debris.replace-when-mined"); + SETTINGS_MAP.put("replace-when-mined", "anti-netherite.ancient-debris.replace-when-mined"); SETTINGS_MAP.put("replace-on-chunk-load", "anti-netherite.ancient-debris.replace-on-chunk-load"); SETTINGS_MAP.put("only-replace-generated-chunks", "anti-netherite.ancient-debris.only-replace-generated-chunks"); SETTINGS_MAP.put("ensure-chunks-loaded", "anti-netherite.ancient-debris.ensure-chunks-loaded"); @@ -81,14 +80,22 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage(Component.text("AntiNetherite configuration reloaded.").color(NamedTextColor.GREEN)); return true; case "restore-debris": - // Check for cooldown to prevent command spam + // Get the configured cooldown in milliseconds + int cooldownSeconds = plugin.getConfig().getInt("anti-netherite.advanced.command-cooldown-seconds", 5); + long cooldownMs = cooldownSeconds * 1000L; + + // Check for cooldown per sender to prevent command spam + String senderKey = sender.getName(); long currentTime = System.currentTimeMillis(); - if (currentTime - lastRestoreTime < RESTORE_COOLDOWN) { - sender.sendMessage(Component.text("Please wait before using this command again.").color(NamedTextColor.RED)); + Long lastTime = lastRestoreTimes.get(senderKey); + + if (lastTime != null && (currentTime - lastTime) < cooldownMs) { + long remainingSeconds = (cooldownMs - (currentTime - lastTime)) / 1000; + sender.sendMessage(Component.text("Please wait " + remainingSeconds + " seconds before using this command again.").color(NamedTextColor.RED)); return true; } - lastRestoreTime = currentTime; + lastRestoreTimes.put(senderKey, currentTime); // Check if replacement features are enabled in config boolean replaceWhenMined = (boolean) plugin.getConfigValue("anti-netherite.ancient-debris.replace-when-mined"); @@ -124,20 +131,13 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage(Component.text("Restoring " + worldLocations + " Ancient Debris in world " + worldName + "...").color(NamedTextColor.YELLOW)); - // Run the restoration asynchronously to prevent lag - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + // Run the restoration on the main thread (delegating Folia handling to DebrisStorage) + Bukkit.getScheduler().runTask(plugin, () -> { try { int count = plugin.getDebrisStorage().restoreDebrisInWorld(world); - - // Send message on the main thread - Bukkit.getScheduler().runTask(plugin, () -> { - sender.sendMessage(Component.text("Restored " + count + " Ancient Debris blocks in world " + worldName + ".").color(NamedTextColor.GREEN)); - }); + sender.sendMessage(Component.text("Restored " + count + " Ancient Debris blocks in world " + worldName + ".").color(NamedTextColor.GREEN)); } catch (Exception e) { - // Send error message on the main thread - Bukkit.getScheduler().runTask(plugin, () -> { - sender.sendMessage(Component.text("Error restoring Ancient Debris: " + e.getMessage()).color(NamedTextColor.RED)); - }); + sender.sendMessage(Component.text("Error restoring Ancient Debris: " + e.getMessage()).color(NamedTextColor.RED)); plugin.getLogger().severe("Error restoring Ancient Debris: " + e.getMessage()); e.printStackTrace(); } @@ -153,20 +153,13 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage(Component.text("Restoring " + totalLocations + " replaced Ancient Debris...").color(NamedTextColor.YELLOW)); - // Run the restoration asynchronously to prevent lag - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + // Run the restoration on the main thread (delegating Folia handling to DebrisStorage) + Bukkit.getScheduler().runTask(plugin, () -> { try { int count = plugin.getDebrisStorage().restoreAllDebris(); - - // Send message on the main thread - Bukkit.getScheduler().runTask(plugin, () -> { - sender.sendMessage(Component.text("Restored " + count + " Ancient Debris blocks.").color(NamedTextColor.GREEN)); - }); + sender.sendMessage(Component.text("Restored " + count + " Ancient Debris blocks.").color(NamedTextColor.GREEN)); } catch (Exception e) { - // Send error message on the main thread - Bukkit.getScheduler().runTask(plugin, () -> { - sender.sendMessage(Component.text("Error restoring Ancient Debris: " + e.getMessage()).color(NamedTextColor.RED)); - }); + sender.sendMessage(Component.text("Error restoring Ancient Debris: " + e.getMessage()).color(NamedTextColor.RED)); plugin.getLogger().severe("Error restoring Ancient Debris: " + e.getMessage()); e.printStackTrace(); } @@ -212,36 +205,10 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } - // Get the actual config path from the user-friendly name - String settingPath = SETTINGS_MAP.get(args[1]); - if (settingPath == null && !args[1].equals("detection.items")) { - sender.sendMessage(Component.text("Unknown setting: " + args[1]).color(NamedTextColor.RED)); - return true; - } - - String settingValue = args[2]; + String settingName = args[1].toLowerCase(); - // Handle different setting types - if (args[1].equals("delay") || args[1].equals("multiplier")) { - try { - int intValue = Integer.parseInt(settingValue); - if (intValue < 1) { - sender.sendMessage(Component.text("Value must be at least 1.").color(NamedTextColor.RED)); - return true; - } - plugin.updateConfig(settingPath, intValue); - } catch (NumberFormatException e) { - sender.sendMessage(Component.text("Invalid number: " + settingValue).color(NamedTextColor.RED)); - return true; - } - } else if (isBooleanSetting(args[1])) { - if (!settingValue.equalsIgnoreCase("true") && !settingValue.equalsIgnoreCase("false")) { - sender.sendMessage(Component.text("Value must be true or false.").color(NamedTextColor.RED)); - return true; - } - plugin.updateConfig(settingPath, Boolean.parseBoolean(settingValue)); - } else if (args[1].equals("detection.items")) { - // Special handling for adding/removing items + // Special handling for detection.items add/remove + if (settingName.equals("detection.items")) { if (args.length < 4) { sender.sendMessage(Component.text("Usage: /antinetherite set detection.items ").color(NamedTextColor.RED)); return true; @@ -272,12 +239,57 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage(Component.text("Invalid action: " + action + ". Use 'add' or 'remove'.").color(NamedTextColor.RED)); } return true; - } else { - sender.sendMessage(Component.text("Unknown setting: " + args[1]).color(NamedTextColor.RED)); + } + + // Get the actual config path from the user-friendly name + String settingPath = getConfigPath(settingName); + if (settingPath == null) { + sender.sendMessage(Component.text("Unknown setting: " + settingName).color(NamedTextColor.RED)); + return true; + } + + String settingValue = args[2]; + + // Get the current value to determine the type + Object currentValue = plugin.getConfigValue(settingPath); + + if (currentValue == null) { + sender.sendMessage(Component.text("Setting not found: " + settingName).color(NamedTextColor.RED)); + return true; + } + + // Type-aware setting + try { + if (currentValue instanceof Boolean) { + // Boolean setting + if (!settingValue.equalsIgnoreCase("true") && !settingValue.equalsIgnoreCase("false")) { + sender.sendMessage(Component.text("Value must be true or false.").color(NamedTextColor.RED)); + return true; + } + plugin.updateConfig(settingPath, Boolean.parseBoolean(settingValue)); + } else if (currentValue instanceof Integer) { + // Integer setting + int intValue = Integer.parseInt(settingValue); + if (intValue < 1) { + sender.sendMessage(Component.text("Value must be at least 1.").color(NamedTextColor.RED)); + return true; + } + plugin.updateConfig(settingPath, intValue); + } else if (currentValue instanceof List) { + // List setting (should not happen here - detection.items is handled above) + sender.sendMessage(Component.text("This setting is a list and cannot be set directly.").color(NamedTextColor.RED)); + return true; + } else { + // Unknown type, try string + plugin.updateConfig(settingPath, settingValue); + } + + sender.sendMessage(Component.text("Set " + settingName + " to " + settingValue).color(NamedTextColor.GREEN)); + } catch (NumberFormatException e) { + sender.sendMessage(Component.text("Invalid number: " + settingValue).color(NamedTextColor.RED)); return true; } - sender.sendMessage(Component.text("Set " + args[1] + " to " + settingValue).color(NamedTextColor.GREEN)); return true; default: showHelp(sender); @@ -285,23 +297,6 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } } - private boolean isBooleanSetting(String setting) { - String configPath = SETTINGS_MAP.get(setting); - if (configPath == null) { - return false; - } - - // Check if the setting is a boolean type - return configPath.contains("clear") || - configPath.contains("cancel") || - configPath.contains("remove") || - configPath.contains("replace") || - configPath.contains("use-name-matching") || - configPath.contains("only-replace-generated-chunks") || - configPath.contains("ensure-chunks-loaded") || - configPath.contains("enable-destructive"); - } - private void showHelp(CommandSender sender) { sender.sendMessage(Component.text("AntiNetherite Commands:").color(NamedTextColor.GOLD)); sender.sendMessage(Component.text("/antinetherite reload - Reload the configuration").color(NamedTextColor.YELLOW)); @@ -325,7 +320,7 @@ private void showHelp(CommandSender sender) { sender.sendMessage(Component.text(" cancel-pickup, remove-dropped").color(NamedTextColor.GRAY)); sender.sendMessage(Component.text("Ancient debris settings:").color(NamedTextColor.YELLOW)); - sender.sendMessage(Component.text(" replace-ancient-debris, replace-on-chunk-load").color(NamedTextColor.GRAY)); + sender.sendMessage(Component.text(" replace-when-mined, replace-on-chunk-load").color(NamedTextColor.GRAY)); sender.sendMessage(Component.text(" only-replace-generated-chunks, ensure-chunks-loaded").color(NamedTextColor.GRAY)); sender.sendMessage(Component.text("Detection settings:").color(NamedTextColor.YELLOW)); diff --git a/src/main/java/top/modpotato/listeners/AttackListener.java b/src/main/java/top/modpotato/listeners/AttackListener.java index e6c0520..a7cf0cd 100644 --- a/src/main/java/top/modpotato/listeners/AttackListener.java +++ b/src/main/java/top/modpotato/listeners/AttackListener.java @@ -37,6 +37,11 @@ public void onAttack(EntityDamageByEntityEvent event) { Player player = (Player) event.getDamager(); + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + // Skip players in creative or spectator mode if configured to do so if (config.isIgnoreCreativeSpectator() && (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR)) { diff --git a/src/main/java/top/modpotato/listeners/CraftListener.java b/src/main/java/top/modpotato/listeners/CraftListener.java index aa01cef..a67488d 100644 --- a/src/main/java/top/modpotato/listeners/CraftListener.java +++ b/src/main/java/top/modpotato/listeners/CraftListener.java @@ -37,6 +37,11 @@ public void onCraftItem(CraftItemEvent event) { Player player = (Player) event.getWhoClicked(); + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + // Skip players in creative or spectator mode if configured to do so if (config.isIgnoreCreativeSpectator() && (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR)) { diff --git a/src/main/java/top/modpotato/listeners/DropListener.java b/src/main/java/top/modpotato/listeners/DropListener.java index 49e56bd..97be5aa 100644 --- a/src/main/java/top/modpotato/listeners/DropListener.java +++ b/src/main/java/top/modpotato/listeners/DropListener.java @@ -38,6 +38,13 @@ public void onDrop(PlayerDropItemEvent event) { return; } + Player player = event.getPlayer(); + + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + ItemStack item = event.getItemDrop().getItemStack(); if (item != null && netheriteDetector.isNetheriteItem(item)) { if (config.isEnableDestructiveActions()) { diff --git a/src/main/java/top/modpotato/listeners/EquipListener.java b/src/main/java/top/modpotato/listeners/EquipListener.java index bcedcb7..5c26906 100644 --- a/src/main/java/top/modpotato/listeners/EquipListener.java +++ b/src/main/java/top/modpotato/listeners/EquipListener.java @@ -39,6 +39,12 @@ public void onInventoryClick(InventoryClickEvent event) { if (!(event.getWhoClicked() instanceof Player player)) { return; } + + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + if (shouldIgnore(player)) { return; } @@ -92,6 +98,12 @@ public void onInventoryDrag(InventoryDragEvent event) { if (!(event.getWhoClicked() instanceof Player player)) { return; } + + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + if (shouldIgnore(player)) { return; } @@ -118,6 +130,12 @@ public void onPlayerInteract(PlayerInteractEvent event) { return; } Player player = event.getPlayer(); + + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + if (shouldIgnore(player)) { return; } @@ -144,6 +162,12 @@ public void onBlockDispenseArmor(BlockDispenseArmorEvent event) { if (!(event.getTargetEntity() instanceof Player player)) { return; } + + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + if (shouldIgnore(player)) { return; } diff --git a/src/main/java/top/modpotato/listeners/InventoryMoveListener.java b/src/main/java/top/modpotato/listeners/InventoryMoveListener.java index 8f920dc..2ac2da6 100644 --- a/src/main/java/top/modpotato/listeners/InventoryMoveListener.java +++ b/src/main/java/top/modpotato/listeners/InventoryMoveListener.java @@ -41,6 +41,11 @@ public void onInventoryClick(InventoryClickEvent event) { Player player = (Player) event.getWhoClicked(); + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + // Skip players in creative or spectator mode if configured to do so if (config.isIgnoreCreativeSpectator() && (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR)) { @@ -127,6 +132,11 @@ public void onInventoryDrag(InventoryDragEvent event) { Player player = (Player) event.getWhoClicked(); + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + // Skip players in creative or spectator mode if configured to do so if (config.isIgnoreCreativeSpectator() && (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR)) { diff --git a/src/main/java/top/modpotato/listeners/MiningListener.java b/src/main/java/top/modpotato/listeners/MiningListener.java index e7ba04f..5cffe50 100644 --- a/src/main/java/top/modpotato/listeners/MiningListener.java +++ b/src/main/java/top/modpotato/listeners/MiningListener.java @@ -69,8 +69,14 @@ public void onBlockDamage(BlockDamageEvent event) { return; } - // Skip if player is in creative or spectator mode and we're ignoring those modes Player player = event.getPlayer(); + + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + + // Skip if player is in creative or spectator mode and we're ignoring those modes if (config.isIgnoreCreativeSpectator() && (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR)) { return; diff --git a/src/main/java/top/modpotato/listeners/PickupListener.java b/src/main/java/top/modpotato/listeners/PickupListener.java index a629076..b802f65 100644 --- a/src/main/java/top/modpotato/listeners/PickupListener.java +++ b/src/main/java/top/modpotato/listeners/PickupListener.java @@ -37,6 +37,11 @@ public void onPickup(EntityPickupItemEvent event) { Player player = (Player) event.getEntity(); + // Skip players with bypass permission + if (player.hasPermission("antinetherite.bypass")) { + return; + } + // Skip players in creative or spectator mode if configured to do so if (config.isIgnoreCreativeSpectator() && (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR)) { diff --git a/src/main/java/top/modpotato/scheduler/NetheriteRemover.java b/src/main/java/top/modpotato/scheduler/NetheriteRemover.java index f72d36c..7ccf2e2 100644 --- a/src/main/java/top/modpotato/scheduler/NetheriteRemover.java +++ b/src/main/java/top/modpotato/scheduler/NetheriteRemover.java @@ -10,8 +10,9 @@ import top.modpotato.config.Config; import top.modpotato.util.NetheriteDetector; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; @@ -31,7 +32,8 @@ public class NetheriteRemover { // Task references for both Bukkit and Folia private BukkitTask bukkitTask; - private List foliaTasks; + private Map foliaPlayerTasks; + private ScheduledTask foliaGlobalTask; /** * Creates a new NetheriteRemover @@ -46,7 +48,7 @@ public NetheriteRemover(Main plugin, boolean isFolia, NetheriteDetector netherit this.netheriteDetector = netheriteDetector; this.logger = plugin.getLogger(); this.config = config; - this.foliaTasks = new ArrayList<>(); + this.foliaPlayerTasks = new HashMap<>(); } /** @@ -107,30 +109,32 @@ private void stopBukkitTask() { * @param delay The delay between checks in ticks */ private void startFoliaTask(int delay) { - foliaTasks = new ArrayList<>(); + foliaPlayerTasks = new HashMap<>(); - // for Folia, we need to schedule a task for each player + // Schedule a task for each online player for (Player player : Bukkit.getOnlinePlayers()) { schedulePlayerTask(player, delay); } - // schedule a global task to handle new players - ScheduledTask globalTask = Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, (task) -> { + // Schedule a global task to handle new players and cleanup + foliaGlobalTask = Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, (task) -> { + // Clean up tasks for offline players + foliaPlayerTasks.entrySet().removeIf(entry -> { + Player player = Bukkit.getPlayer(entry.getKey()); + if (player == null || !player.isOnline()) { + entry.getValue().cancel(); + return true; + } + return false; + }); + + // Schedule tasks for new players for (Player player : Bukkit.getOnlinePlayers()) { - // check if this player already has a task - for (ScheduledTask playerTask : foliaTasks) { - // assume playerTask is cancelled if the player is offline - if (playerTask.isCancelled()) { - foliaTasks.remove(playerTask); - } + if (!foliaPlayerTasks.containsKey(player.getUniqueId())) { + schedulePlayerTask(player, delay); } - - // schedule a new task for this player - schedulePlayerTask(player, delay); } - }, 1, delay); - - foliaTasks.add(globalTask); + }, delay, delay); } /** @@ -139,6 +143,14 @@ private void startFoliaTask(int delay) { * @param delay The delay between checks in ticks */ private void schedulePlayerTask(Player player, int delay) { + UUID playerId = player.getUniqueId(); + + // Cancel existing task if present + ScheduledTask existingTask = foliaPlayerTasks.get(playerId); + if (existingTask != null && !existingTask.isCancelled()) { + existingTask.cancel(); + } + ScheduledTask task = player.getScheduler().runAtFixedRate(plugin, (scheduledTask) -> { AtomicInteger removedCount = new AtomicInteger(0); checkPlayerInventory(player, removedCount); @@ -147,24 +159,33 @@ private void schedulePlayerTask(Player player, int delay) { logger.info("Removed " + removedCount.get() + " Netherite items from " + player.getName() + "'s inventory"); } - // if the player is offline, cancel this task + // If the player is offline, cancel this task and remove from map if (!player.isOnline()) { scheduledTask.cancel(); - foliaTasks.remove(scheduledTask); + foliaPlayerTasks.remove(playerId); } }, null, 1, delay); - foliaTasks.add(task); + foliaPlayerTasks.put(playerId, task); } /** * Stops all Folia tasks */ private void stopFoliaTasks() { - for (ScheduledTask task : foliaTasks) { - task.cancel(); + // Cancel all player tasks + for (ScheduledTask task : foliaPlayerTasks.values()) { + if (task != null && !task.isCancelled()) { + task.cancel(); + } + } + foliaPlayerTasks.clear(); + + // Cancel global task + if (foliaGlobalTask != null && !foliaGlobalTask.isCancelled()) { + foliaGlobalTask.cancel(); + foliaGlobalTask = null; } - foliaTasks.clear(); } /** diff --git a/src/main/java/top/modpotato/util/DebrisStorage.java b/src/main/java/top/modpotato/util/DebrisStorage.java index 1e587b7..85cd0d3 100644 --- a/src/main/java/top/modpotato/util/DebrisStorage.java +++ b/src/main/java/top/modpotato/util/DebrisStorage.java @@ -18,6 +18,8 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; /** @@ -238,7 +240,8 @@ public boolean removeLocation(Location location) { * @return The number of blocks restored */ public int restoreAllDebris() { - int restoredCount = 0; + AtomicInteger restoredCount = new AtomicInteger(0); + boolean isFolia = checkFolia(); for (Map.Entry> entry : replacedLocations.entrySet()) { UUID worldUUID = entry.getKey(); @@ -247,28 +250,77 @@ public int restoreAllDebris() { if (world != null) { List toRemove = new ArrayList<>(); - for (String locString : entry.getValue()) { - try { - Location location = deserializeLocation(world, locString); - - // Check if the chunk is loaded or should be loaded - if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { - plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); - continue; + if (isFolia) { + // On Folia, schedule each block change on the region scheduler + CountDownLatch latch = new CountDownLatch(entry.getValue().size()); + + for (String locString : entry.getValue()) { + try { + Location location = deserializeLocation(world, locString); + + // Check if the chunk is loaded or should be loaded + if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { + plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); + toRemove.add(locString); + latch.countDown(); + continue; + } + + // Schedule block change on the region scheduler + world.getRegionScheduler().execute(plugin, location, () -> { + try { + Block block = location.getBlock(); + + // Only restore if the block is still Netherrack + if (block.getType() == Material.NETHERRACK) { + block.setType(Material.ANCIENT_DEBRIS); + restoredCount.incrementAndGet(); + } + + toRemove.add(locString); + } finally { + latch.countDown(); + } + }); + } catch (Exception e) { + plugin.getLogger().warning("Error restoring Ancient Debris at " + locString + ": " + e.getMessage()); + toRemove.add(locString); // Remove invalid locations + latch.countDown(); } - - Block block = location.getBlock(); - - // Only restore if the block is still Netherrack - if (block.getType() == Material.NETHERRACK) { - block.setType(Material.ANCIENT_DEBRIS); - restoredCount++; + } + + // Wait for all tasks to complete + try { + latch.await(); + } catch (InterruptedException e) { + plugin.getLogger().warning("Interrupted while waiting for debris restoration: " + e.getMessage()); + Thread.currentThread().interrupt(); + } + } else { + // On Paper/Spigot, schedule all block changes on the main thread + for (String locString : entry.getValue()) { + try { + Location location = deserializeLocation(world, locString); + + // Check if the chunk is loaded or should be loaded + if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { + plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); + continue; + } + + Block block = location.getBlock(); + + // Only restore if the block is still Netherrack + if (block.getType() == Material.NETHERRACK) { + block.setType(Material.ANCIENT_DEBRIS); + restoredCount.incrementAndGet(); + } + + toRemove.add(locString); + } catch (Exception e) { + plugin.getLogger().warning("Error restoring Ancient Debris at " + locString + ": " + e.getMessage()); + toRemove.add(locString); // Remove invalid locations } - - toRemove.add(locString); - } catch (Exception e) { - plugin.getLogger().warning("Error restoring Ancient Debris at " + locString + ": " + e.getMessage()); - toRemove.add(locString); // Remove invalid locations } } @@ -277,11 +329,11 @@ public int restoreAllDebris() { } } - // Save changes - saveStorage(); + // Save changes asynchronously + saveStorageAsync(); - plugin.getLogger().info("Restored " + restoredCount + " Ancient Debris blocks"); - return restoredCount; + plugin.getLogger().info("Restored " + restoredCount.get() + " Ancient Debris blocks"); + return restoredCount.get(); } /** @@ -294,46 +346,97 @@ public int restoreDebrisInWorld(World world) { return 0; } - int restoredCount = 0; + AtomicInteger restoredCount = new AtomicInteger(0); UUID worldUUID = world.getUID(); + boolean isFolia = checkFolia(); if (replacedLocations.containsKey(worldUUID)) { List toRemove = new ArrayList<>(); + List locations = new ArrayList<>(replacedLocations.get(worldUUID)); - for (String locString : replacedLocations.get(worldUUID)) { - try { - Location location = deserializeLocation(world, locString); - - // Check if the chunk is loaded or should be loaded - if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { - plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); - continue; + if (isFolia) { + // On Folia, schedule each block change on the region scheduler + CountDownLatch latch = new CountDownLatch(locations.size()); + + for (String locString : locations) { + try { + Location location = deserializeLocation(world, locString); + + // Check if the chunk is loaded or should be loaded + if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { + plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); + toRemove.add(locString); + latch.countDown(); + continue; + } + + // Schedule block change on the region scheduler + world.getRegionScheduler().execute(plugin, location, () -> { + try { + Block block = location.getBlock(); + + // Only restore if the block is still Netherrack + if (block.getType() == Material.NETHERRACK) { + block.setType(Material.ANCIENT_DEBRIS); + restoredCount.incrementAndGet(); + } + + toRemove.add(locString); + } finally { + latch.countDown(); + } + }); + } catch (Exception e) { + plugin.getLogger().warning("Error restoring Ancient Debris at " + locString + ": " + e.getMessage()); + toRemove.add(locString); // Remove invalid locations + latch.countDown(); } - - Block block = location.getBlock(); - - // Only restore if the block is still Netherrack - if (block.getType() == Material.NETHERRACK) { - block.setType(Material.ANCIENT_DEBRIS); - restoredCount++; + } + + // Wait for all tasks to complete + try { + latch.await(); + } catch (InterruptedException e) { + plugin.getLogger().warning("Interrupted while waiting for debris restoration: " + e.getMessage()); + Thread.currentThread().interrupt(); + } + } else { + // On Paper/Spigot, execute block changes on the main thread + for (String locString : locations) { + try { + Location location = deserializeLocation(world, locString); + + // Check if the chunk is loaded or should be loaded + if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { + plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); + continue; + } + + Block block = location.getBlock(); + + // Only restore if the block is still Netherrack + if (block.getType() == Material.NETHERRACK) { + block.setType(Material.ANCIENT_DEBRIS); + restoredCount.incrementAndGet(); + } + + toRemove.add(locString); + } catch (Exception e) { + plugin.getLogger().warning("Error restoring Ancient Debris at " + locString + ": " + e.getMessage()); + toRemove.add(locString); // Remove invalid locations } - - toRemove.add(locString); - } catch (Exception e) { - plugin.getLogger().warning("Error restoring Ancient Debris at " + locString + ": " + e.getMessage()); - toRemove.add(locString); // Remove invalid locations } } // Remove restored locations replacedLocations.get(worldUUID).removeAll(toRemove); - // Save changes - saveStorage(); + // Save changes asynchronously + saveStorageAsync(); } - plugin.getLogger().info("Restored " + restoredCount + " Ancient Debris blocks in world " + world.getName()); - return restoredCount; + plugin.getLogger().info("Restored " + restoredCount.get() + " Ancient Debris blocks in world " + world.getName()); + return restoredCount.get(); } /** @@ -466,4 +569,17 @@ private Location deserializeLocation(World world, String locString) { throw new IllegalArgumentException("Invalid location format: " + locString, e); } } + + /** + * Checks if the server is running on Folia + * @return true if running on Folia, false otherwise + */ + private boolean checkFolia() { + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } } \ No newline at end of file From 49aa7e7a359c1740336aff0bbb99e9844cb9c570 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:24:25 +0000 Subject: [PATCH 4/7] Fix thread-safety issues in debris restoration and Folia task timing Co-authored-by: modpotato <81768237+modpotato@users.noreply.github.com> --- .../modpotato/scheduler/NetheriteRemover.java | 2 +- .../top/modpotato/util/DebrisStorage.java | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/top/modpotato/scheduler/NetheriteRemover.java b/src/main/java/top/modpotato/scheduler/NetheriteRemover.java index 7ccf2e2..002bebe 100644 --- a/src/main/java/top/modpotato/scheduler/NetheriteRemover.java +++ b/src/main/java/top/modpotato/scheduler/NetheriteRemover.java @@ -134,7 +134,7 @@ private void startFoliaTask(int delay) { schedulePlayerTask(player, delay); } } - }, delay, delay); + }, 1, delay); } /** diff --git a/src/main/java/top/modpotato/util/DebrisStorage.java b/src/main/java/top/modpotato/util/DebrisStorage.java index 85cd0d3..20f0278 100644 --- a/src/main/java/top/modpotato/util/DebrisStorage.java +++ b/src/main/java/top/modpotato/util/DebrisStorage.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -248,7 +249,8 @@ public int restoreAllDebris() { World world = Bukkit.getWorld(worldUUID); if (world != null) { - List toRemove = new ArrayList<>(); + // Use thread-safe collection for Folia + ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); if (isFolia) { // On Folia, schedule each block change on the region scheduler @@ -324,8 +326,10 @@ public int restoreAllDebris() { } } - // Remove restored locations - entry.getValue().removeAll(toRemove); + // Thread-safe removal from the original list + synchronized (entry.getValue()) { + entry.getValue().removeAll(toRemove); + } } } @@ -351,7 +355,8 @@ public int restoreDebrisInWorld(World world) { boolean isFolia = checkFolia(); if (replacedLocations.containsKey(worldUUID)) { - List toRemove = new ArrayList<>(); + // Use thread-safe collection for Folia + ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); List locations = new ArrayList<>(replacedLocations.get(worldUUID)); if (isFolia) { @@ -428,8 +433,11 @@ public int restoreDebrisInWorld(World world) { } } - // Remove restored locations - replacedLocations.get(worldUUID).removeAll(toRemove); + // Thread-safe removal from the original list + List worldLocations = replacedLocations.get(worldUUID); + synchronized (worldLocations) { + worldLocations.removeAll(toRemove); + } // Save changes asynchronously saveStorageAsync(); From 5fb7b11ee8562efd25e00554b9bb4a58c5a7eeac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:26:45 +0000 Subject: [PATCH 5/7] Optimize collection usage and fix skipped location handling consistency Co-authored-by: modpotato <81768237+modpotato@users.noreply.github.com> --- .../top/modpotato/util/DebrisStorage.java | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/main/java/top/modpotato/util/DebrisStorage.java b/src/main/java/top/modpotato/util/DebrisStorage.java index 20f0278..a61d791 100644 --- a/src/main/java/top/modpotato/util/DebrisStorage.java +++ b/src/main/java/top/modpotato/util/DebrisStorage.java @@ -249,10 +249,10 @@ public int restoreAllDebris() { World world = Bukkit.getWorld(worldUUID); if (world != null) { - // Use thread-safe collection for Folia - ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); - if (isFolia) { + // On Folia, use thread-safe collection + ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); + // On Folia, schedule each block change on the region scheduler CountDownLatch latch = new CountDownLatch(entry.getValue().size()); @@ -298,8 +298,15 @@ public int restoreAllDebris() { plugin.getLogger().warning("Interrupted while waiting for debris restoration: " + e.getMessage()); Thread.currentThread().interrupt(); } + + // Thread-safe removal from the original list + synchronized (entry.getValue()) { + entry.getValue().removeAll(toRemove); + } } else { - // On Paper/Spigot, schedule all block changes on the main thread + // On Paper/Spigot, use simple ArrayList (single-threaded) + List toRemove = new ArrayList<>(); + for (String locString : entry.getValue()) { try { Location location = deserializeLocation(world, locString); @@ -307,6 +314,7 @@ public int restoreAllDebris() { // Check if the chunk is loaded or should be loaded if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); + toRemove.add(locString); // Add to toRemove for consistency continue; } @@ -324,10 +332,8 @@ public int restoreAllDebris() { toRemove.add(locString); // Remove invalid locations } } - } - - // Thread-safe removal from the original list - synchronized (entry.getValue()) { + + // Remove restored locations (no sync needed - single thread) entry.getValue().removeAll(toRemove); } } @@ -355,11 +361,12 @@ public int restoreDebrisInWorld(World world) { boolean isFolia = checkFolia(); if (replacedLocations.containsKey(worldUUID)) { - // Use thread-safe collection for Folia - ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); List locations = new ArrayList<>(replacedLocations.get(worldUUID)); if (isFolia) { + // On Folia, use thread-safe collection + ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); + // On Folia, schedule each block change on the region scheduler CountDownLatch latch = new CountDownLatch(locations.size()); @@ -405,8 +412,16 @@ public int restoreDebrisInWorld(World world) { plugin.getLogger().warning("Interrupted while waiting for debris restoration: " + e.getMessage()); Thread.currentThread().interrupt(); } + + // Thread-safe removal from the original list + List worldLocations = replacedLocations.get(worldUUID); + synchronized (worldLocations) { + worldLocations.removeAll(toRemove); + } } else { - // On Paper/Spigot, execute block changes on the main thread + // On Paper/Spigot, use simple ArrayList (single-threaded) + List toRemove = new ArrayList<>(); + for (String locString : locations) { try { Location location = deserializeLocation(world, locString); @@ -414,6 +429,7 @@ public int restoreDebrisInWorld(World world) { // Check if the chunk is loaded or should be loaded if (!isChunkLoaded(location) && !loadChunkIfNeeded(location)) { plugin.getLogger().fine("Skipping restoration at " + locString + " because chunk is not loaded"); + toRemove.add(locString); // Add to toRemove for consistency continue; } @@ -431,12 +447,9 @@ public int restoreDebrisInWorld(World world) { toRemove.add(locString); // Remove invalid locations } } - } - - // Thread-safe removal from the original list - List worldLocations = replacedLocations.get(worldUUID); - synchronized (worldLocations) { - worldLocations.removeAll(toRemove); + + // Remove restored locations (no sync needed - single thread) + replacedLocations.get(worldUUID).removeAll(toRemove); } // Save changes asynchronously From 00d2f303a099903bfec33677e18c2edd2d9d24f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:29:45 +0000 Subject: [PATCH 6/7] Add missing Player import, fix timeout handling, and clarify comments Co-authored-by: modpotato <81768237+modpotato@users.noreply.github.com> --- .../top/modpotato/listeners/DropListener.java | 1 + .../top/modpotato/scheduler/NetheriteRemover.java | 2 +- .../java/top/modpotato/util/DebrisStorage.java | 15 +++++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/top/modpotato/listeners/DropListener.java b/src/main/java/top/modpotato/listeners/DropListener.java index 97be5aa..3136095 100644 --- a/src/main/java/top/modpotato/listeners/DropListener.java +++ b/src/main/java/top/modpotato/listeners/DropListener.java @@ -1,5 +1,6 @@ package top.modpotato.listeners; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerDropItemEvent; diff --git a/src/main/java/top/modpotato/scheduler/NetheriteRemover.java b/src/main/java/top/modpotato/scheduler/NetheriteRemover.java index 002bebe..15fe0fb 100644 --- a/src/main/java/top/modpotato/scheduler/NetheriteRemover.java +++ b/src/main/java/top/modpotato/scheduler/NetheriteRemover.java @@ -116,7 +116,7 @@ private void startFoliaTask(int delay) { schedulePlayerTask(player, delay); } - // Schedule a global task to handle new players and cleanup + // Schedule a global task to handle new players and cleanup (starts after 1 tick) foliaGlobalTask = Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, (task) -> { // Clean up tasks for offline players foliaPlayerTasks.entrySet().removeIf(entry -> { diff --git a/src/main/java/top/modpotato/util/DebrisStorage.java b/src/main/java/top/modpotato/util/DebrisStorage.java index a61d791..803fa3d 100644 --- a/src/main/java/top/modpotato/util/DebrisStorage.java +++ b/src/main/java/top/modpotato/util/DebrisStorage.java @@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -291,9 +292,12 @@ public int restoreAllDebris() { } } - // Wait for all tasks to complete + // Wait for all tasks to complete (with timeout to prevent indefinite blocking) try { - latch.await(); + if (!latch.await(60, TimeUnit.SECONDS)) { + plugin.getLogger().warning("Timeout waiting for debris restoration in world " + world.getName() + + ". Some blocks may not have been restored."); + } } catch (InterruptedException e) { plugin.getLogger().warning("Interrupted while waiting for debris restoration: " + e.getMessage()); Thread.currentThread().interrupt(); @@ -405,9 +409,12 @@ public int restoreDebrisInWorld(World world) { } } - // Wait for all tasks to complete + // Wait for all tasks to complete (with timeout to prevent indefinite blocking) try { - latch.await(); + if (!latch.await(60, TimeUnit.SECONDS)) { + plugin.getLogger().warning("Timeout waiting for debris restoration in world " + world.getName() + + ". Some blocks may not have been restored."); + } } catch (InterruptedException e) { plugin.getLogger().warning("Interrupted while waiting for debris restoration: " + e.getMessage()); Thread.currentThread().interrupt(); From 4222f2a998467a1176ed3585d51aa69e20007d0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 05:41:27 +0000 Subject: [PATCH 7/7] Fix Folia region scheduler API usage - use Bukkit.getRegionScheduler() instead of world.getRegionScheduler() Co-authored-by: modpotato <81768237+modpotato@users.noreply.github.com> --- src/main/java/top/modpotato/util/DebrisStorage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/top/modpotato/util/DebrisStorage.java b/src/main/java/top/modpotato/util/DebrisStorage.java index 803fa3d..915a62c 100644 --- a/src/main/java/top/modpotato/util/DebrisStorage.java +++ b/src/main/java/top/modpotato/util/DebrisStorage.java @@ -270,7 +270,7 @@ public int restoreAllDebris() { } // Schedule block change on the region scheduler - world.getRegionScheduler().execute(plugin, location, () -> { + Bukkit.getRegionScheduler().execute(plugin, location, () -> { try { Block block = location.getBlock(); @@ -387,7 +387,7 @@ public int restoreDebrisInWorld(World world) { } // Schedule block change on the region scheduler - world.getRegionScheduler().execute(plugin, location, () -> { + Bukkit.getRegionScheduler().execute(plugin, location, () -> { try { Block block = location.getBlock();