From e11bb8c7adb7b288aeb95d184a87d059dddf24a5 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 23:44:03 +0800 Subject: [PATCH 1/5] improve db saving time --- .../milkteamc/autotreechop/AutoTreeChop.java | 18 ++++++- .../database/DatabaseManager.java | 50 +++++++++++-------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 531a060..f09b0ce 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -19,6 +19,8 @@ import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; @@ -269,6 +271,8 @@ public void onDisable() { if (playerConfigs != null && !playerConfigs.isEmpty()) { SessionManager sessionManager = SessionManager.getInstance(); + List dirtyDataList = new ArrayList<>(); + for (Map.Entry entry : playerConfigs.entrySet()) { UUID uuid = entry.getKey(); PlayerConfig pConfig = entry.getValue(); @@ -277,14 +281,24 @@ public void onDisable() { confirmationManager.clearPlayer(uuid); } - if (pConfig.isDirty() && databaseManager != null) { - databaseManager.savePlayerDataSync(pConfig.getData()); + if (pConfig.isDirty()) { + dirtyDataList.add(pConfig.getData()); } if (sessionManager != null) { sessionManager.clearAllPlayerSessions(uuid); } } + + if (!dirtyDataList.isEmpty() && databaseManager != null) { + long startTime = System.currentTimeMillis(); + + databaseManager.savePlayerDataBatchSync(dirtyDataList); + + long duration = System.currentTimeMillis() - startTime; + getLogger().info("Successfully saved player records in " + duration + "ms."); + } + playerConfigs.clear(); } diff --git a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java index c4a37f2..818ddc1 100644 --- a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java +++ b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java @@ -25,6 +25,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.time.LocalDate; +import java.util.Collection; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -124,37 +125,46 @@ public void savePlayerDataSync(PlayerData data) { } } - public CompletableFuture savePlayerDataBatchAsync(Map dataMap) { - return CompletableFuture.runAsync(() -> { - if (dataMap.isEmpty()) return; + public void savePlayerDataBatchSync(Collection dataCollection) { + if (dataCollection == null || dataCollection.isEmpty()) return; - String sql = buildUpsertSql(); - try (Connection conn = dataSource.getConnection()) { - conn.setAutoCommit(false); + String sql = buildUpsertSql(); - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - for (PlayerData data : dataMap.values()) { - bindUpsertParams(stmt, data); - stmt.addBatch(); - } - stmt.executeBatch(); - conn.commit(); - } catch (SQLException e) { - conn.rollback(); - throw e; + try (Connection conn = dataSource.getConnection()) { + boolean originalAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + for (PlayerData data : dataCollection) { + bindUpsertParams(stmt, data); + stmt.addBatch(); } + + stmt.executeBatch(); + conn.commit(); + } catch (SQLException e) { - plugin.getLogger().warning("Error batch saving player data: " + e.getMessage()); + conn.rollback(); + plugin.getLogger().severe("Failed to batch save player data: " + e.getMessage()); + throw e; // 如果是嚴重錯誤,可能需要往上拋或在這裡單純記錄 + } finally { + conn.setAutoCommit(originalAutoCommit); } - }); + } catch (SQLException e) { + plugin.getLogger().severe("Database connection error during batch save: " + e.getMessage()); + } + } + + public CompletableFuture savePlayerDataBatchAsync(Map dataMap) { + return CompletableFuture.runAsync(() -> savePlayerDataBatchSync(dataMap.values())); } /** * Returns a dialect-appropriate UPSERT statement. * *
    - *
  • SQLite: {@code INSERT OR REPLACE INTO ...} - *
  • MySQL: {@code INSERT INTO ... ON DUPLICATE KEY UPDATE ...} + *
  • SQLite: {@code INSERT OR REPLACE INTO ...} + *
  • MySQL: {@code INSERT INTO ... ON DUPLICATE KEY UPDATE ...} *
*/ private String buildUpsertSql() { From 6a52f3afa98156565a957850c42d6e0f1811ee38 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sun, 12 Apr 2026 08:56:26 +0800 Subject: [PATCH 2/5] move to other class --- .../milkteamc/autotreechop/AutoTreeChop.java | 365 +++++------------- .../autotreechop/AutoTreeChopAPI.java | 10 +- .../autotreechop/AutoTreeChopExpansion.java | 7 +- .../autotreechop/command/ConfirmCommand.java | 20 +- .../autotreechop/command/ToggleCommand.java | 12 +- .../autotreechop/command/UsageCommand.java | 3 +- .../autotreechop/database/DataManager.java | 121 ++++++ .../events/BlockBreakListener.java | 63 +-- .../events/PlayerJoinListener.java | 7 +- .../events/PlayerQuitListener.java | 7 +- .../events/PlayerSneakListener.java | 2 +- .../autotreechop/hooks/HookManager.java | 114 ++++++ .../tasks/PlayerDataSaveTask.java | 9 +- 13 files changed, 380 insertions(+), 360 deletions(-) create mode 100644 src/main/java/org/milkteamc/autotreechop/database/DataManager.java create mode 100644 src/main/java/org/milkteamc/autotreechop/hooks/HookManager.java diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index f09b0ce..097b1e5 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -17,14 +17,8 @@ package org.milkteamc.autotreechop; -import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.io.File; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; @@ -36,64 +30,37 @@ import org.milkteamc.autotreechop.command.ReloadCommand; import org.milkteamc.autotreechop.command.ToggleCommand; import org.milkteamc.autotreechop.command.UsageCommand; +import org.milkteamc.autotreechop.database.DataManager; import org.milkteamc.autotreechop.database.DatabaseManager; import org.milkteamc.autotreechop.events.BlockBreakListener; import org.milkteamc.autotreechop.events.PlayerJoinListener; import org.milkteamc.autotreechop.events.PlayerQuitListener; import org.milkteamc.autotreechop.events.PlayerSneakListener; -import org.milkteamc.autotreechop.hooks.GriefPreventionHook; -import org.milkteamc.autotreechop.hooks.LandsHook; -import org.milkteamc.autotreechop.hooks.ResidenceHook; -import org.milkteamc.autotreechop.hooks.WorldGuardHook; -import org.milkteamc.autotreechop.tasks.PlayerDataSaveTask; +import org.milkteamc.autotreechop.hooks.HookManager; import org.milkteamc.autotreechop.translation.TranslationManager; import org.milkteamc.autotreechop.updater.ModrinthUpdateChecker; import org.milkteamc.autotreechop.utils.ConfirmationManager; import org.milkteamc.autotreechop.utils.CooldownManager; -import org.milkteamc.autotreechop.utils.SessionManager; import org.milkteamc.autotreechop.utils.TreeChopUtils; import revxrsal.commands.bukkit.BukkitLamp; public class AutoTreeChop extends JavaPlugin { - private static final long SAVE_INTERVAL = 1200L; // 60s - private static final int SAVE_THRESHOLD = 15; - private static AutoTreeChop instance; private Config config; - private AutoTreeChopAPI autoTreeChopAPI; - private Map playerConfigs = new ConcurrentHashMap<>(); - private Metrics metrics; + private DatabaseManager databaseManager; + private DataManager dataManager; + private HookManager hookManager; private TranslationManager translationManager; - private ConfirmationManager confirmationManager; - private ModrinthUpdateChecker updateChecker; - private PluginDescriptionFile description; - - private boolean worldGuardEnabled = false; - private boolean residenceEnabled = false; - private boolean griefPreventionEnabled = false; - private boolean landsEnabled = false; - private WorldGuardHook worldGuardHook = null; - private ResidenceHook residenceHook = null; - private GriefPreventionHook griefPreventionHook = null; - private LandsHook landsHook = null; + private AutoTreeChopAPI autoTreeChopAPI; + private ConfirmationManager confirmationManager; private CooldownManager cooldownManager; - - private DatabaseManager databaseManager; - private PlayerDataSaveTask saveTask; - private TreeChopUtils treeChopUtils; - - /** - * Sends a translated message to a command sender - */ - public static void sendMessage(CommandSender sender, String messageKey, TagResolver... resolvers) { - if (instance != null && instance.translationManager != null) { - instance.translationManager.sendMessage(sender, messageKey, resolvers); - } - } + private ModrinthUpdateChecker updateChecker; + private Metrics metrics; + private PluginDescriptionFile description; public static boolean isFolia() { try { @@ -104,6 +71,12 @@ public static boolean isFolia() { } } + public static void sendMessage(CommandSender sender, String messageKey, TagResolver... resolvers) { + if (instance != null && instance.translationManager != null) { + instance.translationManager.sendMessage(sender, messageKey, resolvers); + } + } + @Override public void onLoad() { @SuppressWarnings("deprecation") @@ -116,137 +89,51 @@ public void onEnable() { instance = this; saveDefaultConfig(); - config = new Config(this); - - metrics = new Metrics(this, 20053); - - // Register event listeners - registerEvents(); - - // Initialize translation system - translationManager = new TranslationManager(this); - loadLocale(); - - // Register commands - var lamp = BukkitLamp.builder(this).build(); - lamp.register(new ReloadCommand(this, config)); - lamp.register(new AboutCommand(this)); - lamp.register(new ToggleCommand(this)); - lamp.register(new UsageCommand(this, config)); - lamp.register(new ConfirmCommand(this)); + this.config = new Config(this); + setupTranslation(); - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { - new AutoTreeChopExpansion(this).register(); - getLogger().info("PlaceholderAPI expansion for AutoTreeChop has been registered."); - } - - updateChecker = new ModrinthUpdateChecker(this, "autotreechop", "paper") - .setDonationLink("https://ko-fi.com/maoyue") - .setChangelogLink("https://modrinth.com/plugin/autotreechop/changelog") - .setDownloadLink("https://modrinth.com/plugin/autotreechop/versions") - .setNotifyOpsOnJoin(true) - .setNotifyByPermissionOnJoin("autotreechop.updatechecker") - .startPeriodicCheck(); - - databaseManager = new DatabaseManager( - this, - config.isUseMysql(), - config.getHostname(), - config.getPort(), - config.getDatabase(), - config.getUsername(), - config.getPassword()); + this.cooldownManager = new CooldownManager(); + this.confirmationManager = new ConfirmationManager(this); + this.treeChopUtils = new TreeChopUtils(this); + this.autoTreeChopAPI = new AutoTreeChopAPI(this); - saveTask = new PlayerDataSaveTask(this, SAVE_THRESHOLD); - UniversalScheduler.getScheduler(this).runTaskTimerAsynchronously(saveTask, SAVE_INTERVAL, SAVE_INTERVAL); - autoTreeChopAPI = new AutoTreeChopAPI(this); - playerConfigs = new ConcurrentHashMap<>(); - initializeHooks(); + this.hookManager = new HookManager(this, config); - cooldownManager = new CooldownManager(); + setupDatabase(); + this.dataManager = new DataManager(this, databaseManager, confirmationManager); + this.dataManager.startSaveTask(); - confirmationManager = new ConfirmationManager(this); + registerEvents(); + registerCommands(); - this.treeChopUtils = new TreeChopUtils(this); + setupIntegrations(); getLogger().info("AutoTreeChop enabled!"); } - private void registerEvents() { - getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); - getServer().getPluginManager().registerEvents(new PlayerQuitListener(this), this); - getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this); - getServer().getPluginManager().registerEvents(new PlayerSneakListener(this), this); - } - - private void initializeHooks() { - if (Bukkit.getPluginManager().getPlugin("Residence") != null) { - try { - residenceHook = new ResidenceHook(config.getResidenceFlag()); - residenceEnabled = true; - getLogger().info("Residence support enabled"); - } catch (Exception e) { - getLogger() - .warning( - "Residence can't be hook, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); - residenceEnabled = false; - } - } else { - residenceEnabled = false; + @Override + public void onDisable() { + if (dataManager != null) { + dataManager.shutdown(); } - if (Bukkit.getPluginManager().getPlugin("GriefPrevention") != null) { - try { - griefPreventionHook = new GriefPreventionHook(config.getGriefPreventionFlag()); - griefPreventionEnabled = true; - getLogger().info("GriefPrevention support enabled"); - } catch (Exception e) { - getLogger() - .warning( - "GriefPrevention can't be hook, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); - griefPreventionEnabled = false; - } - } else { - griefPreventionEnabled = false; + if (translationManager != null) { + translationManager.close(); } - if (Bukkit.getPluginManager().getPlugin("Lands") != null) { - try { - landsHook = new LandsHook(this); - landsEnabled = true; - getLogger().info("Lands support enabled"); - } catch (Exception e) { - getLogger() - .warning( - "Lands can't be hook, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); - landsEnabled = false; - } - } else { - landsEnabled = false; + if (metrics != null) { + metrics.shutdown(); } - if (Bukkit.getPluginManager().getPlugin("WorldGuard") != null) { - try { - worldGuardHook = new WorldGuardHook(); - worldGuardEnabled = true; - getLogger().info("WorldGuard support enabled"); - } catch (NoClassDefFoundError e) { - getLogger() - .warning( - "WorldGuard can't be hook, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); - worldGuardEnabled = false; - } - } else { - worldGuardEnabled = false; - } + getLogger().info("AutoTreeChop disabled!"); } - private void loadLocale() { + private void setupTranslation() { + this.translationManager = new TranslationManager(this); String[] langs = {"styles", "en", "de", "es", "fr", "ja", "ru", "zh", "ms"}; for (String lang : langs) { saveResourceIfNotExists("lang/" + lang + ".properties"); } - Locale defaultLocale = config.getLocale() == null ? Locale.getDefault() : config.getLocale(); translationManager.initialize(defaultLocale, config.isUseClientLocale()); } @@ -257,159 +144,95 @@ private void saveResourceIfNotExists(String resourcePath) { } } - @Override - public void onDisable() { - getLogger().info("Saving all player data before shutdown..."); - - if (saveTask != null) { - try { - saveTask.cancel(); - } catch (IllegalStateException ignored) { - // Task was never scheduled or already cancelled (e.g. Folia shutdown) - } - } - - if (playerConfigs != null && !playerConfigs.isEmpty()) { - SessionManager sessionManager = SessionManager.getInstance(); - List dirtyDataList = new ArrayList<>(); - - for (Map.Entry entry : playerConfigs.entrySet()) { - UUID uuid = entry.getKey(); - PlayerConfig pConfig = entry.getValue(); - - if (confirmationManager != null) { - confirmationManager.clearPlayer(uuid); - } - - if (pConfig.isDirty()) { - dirtyDataList.add(pConfig.getData()); - } - - if (sessionManager != null) { - sessionManager.clearAllPlayerSessions(uuid); - } - } - - if (!dirtyDataList.isEmpty() && databaseManager != null) { - long startTime = System.currentTimeMillis(); - - databaseManager.savePlayerDataBatchSync(dirtyDataList); - - long duration = System.currentTimeMillis() - startTime; - getLogger().info("Successfully saved player records in " + duration + "ms."); - } - - playerConfigs.clear(); - } - - if (databaseManager != null) { - databaseManager.close(); - } - - if (translationManager != null) { - translationManager.close(); - } - - if (metrics != null) { - metrics.shutdown(); - } - - getLogger().info("AutoTreeChop disabled!"); - } - - public PlayerConfig getPlayerConfig(UUID playerUUID) { - PlayerConfig playerConfig = playerConfigs.get(playerUUID); - - if (playerConfig == null) { - DatabaseManager.PlayerData tempDefaultData = - new DatabaseManager.PlayerData(playerUUID, false, 0, 0, java.time.LocalDate.now()); - return new PlayerConfig(playerUUID, tempDefaultData); - } - - return playerConfig; + private void setupDatabase() { + this.databaseManager = new DatabaseManager( + this, + config.isUseMysql(), + config.getHostname(), + config.getPort(), + config.getDatabase(), + config.getUsername(), + config.getPassword()); } - public int getPlayerDailyUses(UUID playerUUID) { - return getPlayerConfig(playerUUID).getDailyUses(); + private void registerEvents() { + getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerQuitListener(this), this); + getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerSneakListener(this), this); } - public int getPlayerDailyBlocksBroken(UUID playerUUID) { - return getPlayerConfig(playerUUID).getDailyBlocksBroken(); + private void registerCommands() { + var lamp = BukkitLamp.builder(this).build(); + lamp.register(new ReloadCommand(this, config)); + lamp.register(new AboutCommand(this)); + lamp.register(new ToggleCommand(this)); + lamp.register(new UsageCommand(this, config)); + lamp.register(new ConfirmCommand(this)); } - public AutoTreeChopAPI getAutoTreeChopAPI() { - return autoTreeChopAPI; - } + private void setupIntegrations() { + this.metrics = new Metrics(this, 20053); - public ModrinthUpdateChecker getUpdateChecker() { - return updateChecker; - } + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + new AutoTreeChopExpansion(this).register(); + getLogger().info("PlaceholderAPI expansion for AutoTreeChop has been registered."); + } - public PluginDescriptionFile getPluginDescription() { - return description; + this.updateChecker = new ModrinthUpdateChecker(this, "autotreechop", "paper") + .setDonationLink("https://ko-fi.com/maoyue") + .setChangelogLink("https://modrinth.com/plugin/autotreechop/changelog") + .setDownloadLink("https://modrinth.com/plugin/autotreechop/versions") + .setNotifyOpsOnJoin(true) + .setNotifyByPermissionOnJoin("autotreechop.updatechecker") + .startPeriodicCheck(); } - public CooldownManager getCooldownManager() { - return cooldownManager; + public static AutoTreeChop getInstance() { + return instance; } public Config getPluginConfig() { return config; } - public Map getAllPlayerConfigs() { - return playerConfigs; + public DataManager getDataManager() { + return dataManager; } public DatabaseManager getDatabaseManager() { return databaseManager; } - public TreeChopUtils getTreeChopUtils() { - return treeChopUtils; - } - - public TranslationManager getTranslationManager() { - return translationManager; - } - - public ConfirmationManager getConfirmationManager() { - return confirmationManager; - } - - public boolean isWorldGuardEnabled() { - return worldGuardEnabled; - } - - public boolean isResidenceEnabled() { - return residenceEnabled; + public HookManager getHookManager() { + return hookManager; } - public boolean isGriefPreventionEnabled() { - return griefPreventionEnabled; + public AutoTreeChopAPI getAutoTreeChopAPI() { + return autoTreeChopAPI; } - public boolean isLandsEnabled() { - return landsEnabled; + public CooldownManager getCooldownManager() { + return cooldownManager; } - public WorldGuardHook getWorldGuardHook() { - return worldGuardHook; + public ConfirmationManager getConfirmationManager() { + return confirmationManager; } - public ResidenceHook getResidenceHook() { - return residenceHook; + public TreeChopUtils getTreeChopUtils() { + return treeChopUtils; } - public GriefPreventionHook getGriefPreventionHook() { - return griefPreventionHook; + public TranslationManager getTranslationManager() { + return translationManager; } - public LandsHook getLandsHook() { - return landsHook; + public ModrinthUpdateChecker getUpdateChecker() { + return updateChecker; } - public static AutoTreeChop getInstance() { - return instance; + public PluginDescriptionFile getPluginDescription() { + return description; } } diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java index 734a1be..119fabf 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java @@ -34,7 +34,7 @@ public AutoTreeChopAPI(AutoTreeChop plugin) { * @return boolean */ public boolean isAutoTreeChopEnabled(Player player) { - PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(player.getUniqueId()); return playerConfig.isAutoTreeChopEnabled(); } @@ -42,7 +42,7 @@ public boolean isAutoTreeChopEnabled(Player player) { * Set specific player AutoTreeChop as enabled */ public void enableAutoTreeChop(Player player) { - PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(player.getUniqueId()); playerConfig.setAutoTreeChopEnabled(true); } @@ -50,7 +50,7 @@ public void enableAutoTreeChop(Player player) { * Set specific player AutoTreeChop as disable */ public void disableAutoTreeChop(Player player) { - PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(player.getUniqueId()); playerConfig.setAutoTreeChopEnabled(false); } @@ -60,7 +60,7 @@ public void disableAutoTreeChop(Player player) { * @return int */ public int getPlayerDailyUses(UUID playerUUID) { - return plugin.getPlayerDailyUses(playerUUID); + return plugin.getDataManager().getPlayerDailyUses(playerUUID); } /** @@ -69,6 +69,6 @@ public int getPlayerDailyUses(UUID playerUUID) { * @return int */ public int getPlayerDailyBlocksBroken(UUID playerUUID) { - return plugin.getPlayerDailyBlocksBroken(playerUUID); + return plugin.getDataManager().getPlayerDailyBlocksBroken(playerUUID); } } diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java index 3108a38..553f91a 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java @@ -54,11 +54,12 @@ public String onPlaceholderRequest(Player player, @NotNull String params) { UUID playerUUID = player.getUniqueId(); if (params.equalsIgnoreCase("daily_uses")) { - return String.valueOf(plugin.getPlayerDailyUses(playerUUID)); + return String.valueOf(plugin.getDataManager().getPlayerDailyUses(playerUUID)); } else if (params.equalsIgnoreCase("daily_blocks_broken")) { - return String.valueOf(plugin.getPlayerDailyBlocksBroken(playerUUID)); + return String.valueOf(plugin.getDataManager().getPlayerDailyBlocksBroken(playerUUID)); } else if (params.equalsIgnoreCase("status")) { - return String.valueOf(plugin.getPlayerConfig(playerUUID).isAutoTreeChopEnabled()); + return String.valueOf( + plugin.getDataManager().getPlayerConfig(playerUUID).isAutoTreeChopEnabled()); } return null; diff --git a/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java index e488ec3..37757dd 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java @@ -24,6 +24,7 @@ import org.milkteamc.autotreechop.Config; import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; +import org.milkteamc.autotreechop.hooks.HookManager; import org.milkteamc.autotreechop.utils.BlockDiscoveryUtils; import org.milkteamc.autotreechop.utils.ConfirmationManager.ChopData; import org.milkteamc.autotreechop.utils.EffectUtils; @@ -63,7 +64,7 @@ public void confirm(BukkitCommandActor actor) { } Config config = plugin.getPluginConfig(); - PlayerConfig playerConfig = plugin.getPlayerConfig(uuid); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(uuid); // The block may have been broken or replaced during the confirmation window // (e.g. another player cleared it). Re-validate before chopping. @@ -82,15 +83,16 @@ public void confirm(BukkitCommandActor actor) { EffectUtils.showChopEffect(player, block); } + HookManager hookManager = plugin.getHookManager(); ProtectionHooks hooks = new ProtectionHooks( - plugin.isWorldGuardEnabled(), - plugin.getWorldGuardHook(), - plugin.isResidenceEnabled(), - plugin.getResidenceHook(), - plugin.isGriefPreventionEnabled(), - plugin.getGriefPreventionHook(), - plugin.isLandsEnabled(), - plugin.getLandsHook()); + hookManager.isWorldGuardEnabled(), + hookManager.getWorldGuardHook(), + hookManager.isResidenceEnabled(), + hookManager.getResidenceHook(), + hookManager.isGriefPreventionEnabled(), + hookManager.getGriefPreventionHook(), + hookManager.isLandsEnabled(), + hookManager.getLandsHook()); plugin.getTreeChopUtils() .chopTree( diff --git a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java index a564e55..c824a84 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java @@ -58,7 +58,7 @@ public void toggle(BukkitCommandActor actor, @Optional Player targetPlayer) { } UUID targetUUID = targetPlayer.getUniqueId(); - PlayerConfig playerConfig = plugin.getPlayerConfig(targetUUID); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(targetUUID); boolean autoTreeChopEnabled = !playerConfig.isAutoTreeChopEnabled(); playerConfig.setAutoTreeChopEnabled(autoTreeChopEnabled); @@ -96,7 +96,7 @@ public void enable(BukkitCommandActor actor) { AutoTreeChop.sendMessage(actor.sender(), MessageKeys.ONLY_PLAYERS); return; } - PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(player.getUniqueId()); if (playerConfig.isAutoTreeChopEnabled()) { AutoTreeChop.sendMessage(player, MessageKeys.ALREADY_ENABLED); return; @@ -112,7 +112,7 @@ public void enable(BukkitCommandActor actor, EntitySelector targetPlayer int count = 0; String lastName = null; for (Player targetPlayer : targetPlayers) { - PlayerConfig cfg = plugin.getPlayerConfig(targetPlayer.getUniqueId()); + PlayerConfig cfg = plugin.getDataManager().getPlayerConfig(targetPlayer.getUniqueId()); if (cfg.isAutoTreeChopEnabled()) continue; // skip already-enabled silently, or send per-player msg cfg.setAutoTreeChopEnabled(true); lastName = targetPlayer.getName(); @@ -144,7 +144,7 @@ public void disable(BukkitCommandActor actor) { return; } UUID playerUUID = player.getUniqueId(); - PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(playerUUID); if (!playerConfig.isAutoTreeChopEnabled()) { AutoTreeChop.sendMessage(player, MessageKeys.ALREADY_DISABLED); return; @@ -162,7 +162,7 @@ public void disable(BukkitCommandActor actor, EntitySelector targetPlaye String lastName = null; for (Player targetPlayer : targetPlayers) { UUID targetUUID = targetPlayer.getUniqueId(); - PlayerConfig cfg = plugin.getPlayerConfig(targetUUID); + PlayerConfig cfg = plugin.getDataManager().getPlayerConfig(targetUUID); if (!cfg.isAutoTreeChopEnabled()) continue; cfg.setAutoTreeChopEnabled(false); plugin.getConfirmationManager().clearPlayer(targetUUID); @@ -194,7 +194,7 @@ private void performSelfToggle(BukkitCommandActor actor) { } UUID playerUUID = player.getUniqueId(); - PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(playerUUID); boolean autoTreeChopEnabled = !playerConfig.isAutoTreeChopEnabled(); playerConfig.setAutoTreeChopEnabled(autoTreeChopEnabled); diff --git a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java index 46a0956..bc516a6 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java @@ -47,7 +47,8 @@ public void usage(BukkitCommandActor actor) { } Player player = actor.asPlayer(); - org.milkteamc.autotreechop.PlayerConfig pConfig = plugin.getPlayerConfig(player.getUniqueId()); + org.milkteamc.autotreechop.PlayerConfig pConfig = + plugin.getDataManager().getPlayerConfig(player.getUniqueId()); boolean isVip = player.hasPermission("autotreechop.vip"); boolean limitVip = config.getLimitVipUsage(); diff --git a/src/main/java/org/milkteamc/autotreechop/database/DataManager.java b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java new file mode 100644 index 0000000..88344b0 --- /dev/null +++ b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2026 MilkTeaMC and contributors + * + * 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 org.milkteamc.autotreechop.database; + +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.milkteamc.autotreechop.AutoTreeChop; +import org.milkteamc.autotreechop.PlayerConfig; +import org.milkteamc.autotreechop.tasks.PlayerDataSaveTask; +import org.milkteamc.autotreechop.utils.ConfirmationManager; +import org.milkteamc.autotreechop.utils.SessionManager; + +public class DataManager { + + private static final long SAVE_INTERVAL = 1200L; // 60s + private static final int SAVE_THRESHOLD = 15; + + private final AutoTreeChop plugin; + private final DatabaseManager databaseManager; + private final ConfirmationManager confirmationManager; + private final Map playerConfigs = new ConcurrentHashMap<>(); + + private PlayerDataSaveTask saveTask; + + public DataManager(AutoTreeChop plugin, DatabaseManager databaseManager, ConfirmationManager confirmationManager) { + this.plugin = plugin; + this.databaseManager = databaseManager; + this.confirmationManager = confirmationManager; + } + + public void startSaveTask() { + this.saveTask = new PlayerDataSaveTask(plugin, SAVE_THRESHOLD); + UniversalScheduler.getScheduler(plugin).runTaskTimerAsynchronously(saveTask, SAVE_INTERVAL, SAVE_INTERVAL); + } + + public void shutdown() { + plugin.getLogger().info("Saving all player data before shutdown..."); + + if (saveTask != null) { + try { + saveTask.cancel(); + } catch (IllegalStateException ignored) { + // Task was never scheduled or already cancelled (e.g. Folia shutdown) + } + } + + if (!playerConfigs.isEmpty()) { + SessionManager sessionManager = SessionManager.getInstance(); + List dirtyDataList = new ArrayList<>(); + + for (Map.Entry entry : playerConfigs.entrySet()) { + UUID uuid = entry.getKey(); + PlayerConfig pConfig = entry.getValue(); + + if (confirmationManager != null) { + confirmationManager.clearPlayer(uuid); + } + + if (pConfig.isDirty()) { + dirtyDataList.add(pConfig.getData()); + } + + if (sessionManager != null) { + sessionManager.clearAllPlayerSessions(uuid); + } + } + + if (!dirtyDataList.isEmpty() && databaseManager != null) { + long startTime = System.currentTimeMillis(); + databaseManager.savePlayerDataBatchSync(dirtyDataList); + long duration = System.currentTimeMillis() - startTime; + plugin.getLogger().info("Successfully saved player records in " + duration + "ms."); + } + + playerConfigs.clear(); + } + + if (databaseManager != null) { + databaseManager.close(); + } + } + + public PlayerConfig getPlayerConfig(UUID playerUUID) { + return playerConfigs.computeIfAbsent(playerUUID, k -> { + DatabaseManager.PlayerData tempDefaultData = + new DatabaseManager.PlayerData(k, false, 0, 0, java.time.LocalDate.now()); + return new PlayerConfig(k, tempDefaultData); + }); + } + + public int getPlayerDailyUses(UUID playerUUID) { + return getPlayerConfig(playerUUID).getDailyUses(); + } + + public int getPlayerDailyBlocksBroken(UUID playerUUID) { + return getPlayerConfig(playerUUID).getDailyBlocksBroken(); + } + + public Map getAllPlayerConfigs() { + return playerConfigs; + } +} diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 12c6d9e..7d1e114 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -38,6 +38,7 @@ import org.milkteamc.autotreechop.Config; import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; +import org.milkteamc.autotreechop.hooks.HookManager; import org.milkteamc.autotreechop.utils.AsyncTaskScheduler; import org.milkteamc.autotreechop.utils.BlockDiscoveryUtils; import org.milkteamc.autotreechop.utils.ConfirmationManager; @@ -53,16 +54,6 @@ public class BlockBreakListener implements Listener { private final AutoTreeChop plugin; private final AsyncTaskScheduler scheduler; - /** - * Players who currently have an async leaf-check in flight. - * Guards against the race where the player breaks a second log before the - * first async check completes, which would start two concurrent chop pipelines - * for the same player before either has registered with SessionManager. - * - *

A player is added just before the async task is submitted and removed - * (via try-finally) when the sync callback finishes, whether it dispatches a - * chop, sets a pending confirmation, or discards the event (player offline). - */ private final Set leafCheckInProgress = ConcurrentHashMap.newKeySet(); public BlockBreakListener(AutoTreeChop plugin) { @@ -74,7 +65,7 @@ public BlockBreakListener(AutoTreeChop plugin) { public void onBlockBreak(BlockBreakEvent event) { Player player = event.getPlayer(); UUID playerUUID = player.getUniqueId(); - PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(playerUUID); Block block = event.getBlock(); ItemStack tool = player.getInventory().getItemInMainHand(); Location location = block.getLocation(); @@ -117,51 +108,38 @@ public void onBlockBreak(BlockBreakEvent event) { return; } - // Limits cleared — check for a pending confirmation first. ConfirmationManager confirmationManager = plugin.getConfirmationManager(); ChopData pending = confirmationManager.consumePendingConfirmation(playerUUID); event.setCancelled(true); if (pending != null) { - // Player confirmed by breaking a log within the confirmation window. - // Skip the leaf check entirely; grace is determined by the original reason. confirmationManager.recordSuccessfulChop(playerUUID, pending.reason(), false); AutoTreeChop.sendMessage(player, MessageKeys.CONFIRMATION_SUCCESS); dispatchChop(player, playerConfig, block, tool, location, config); return; } - // Guard against concurrent leaf checks for the same player. - // If a check is already in flight we simply eat this break — the log is - // still present (event was cancelled) so the player can try again. if (!leafCheckInProgress.add(playerUUID)) { return; } - // Pre-capture chunk snapshots on the main/region thread (world access is required - // here), then read them on an async thread (snapshots are immutable — thread-safe). int radius = config.getNoLeavesDetectionRadius(); Map snapshots = captureLeafCheckSnapshots(block, radius); - // Clone tool now so we have stable values for the async path. ItemStack frozenTool = tool.clone(); Location frozenLocation = location; scheduler.runTaskAsync(() -> { boolean hasLeaves = hasNearbyLeaves(block, radius, config, snapshots); - // Return to the main/region thread to act on the result. scheduler.runTaskAtLocation(frozenLocation, () -> { - // try-finally guarantees leafCheckInProgress is cleared on every exit path. try { if (!player.isOnline()) return; ConfirmReason reason = confirmationManager.getConfirmationReason(playerUUID, hasLeaves); if (reason != null) { - // Store the chop parameters so /atc confirm can fire the chop - // without requiring the player to physically re-break the log. confirmationManager.setPendingConfirmation(playerUUID, reason, frozenLocation, frozenTool); String timeoutStr = String.valueOf(config.getConfirmationWindowSeconds()); @@ -205,30 +183,19 @@ void dispatchChop( hooks); } - /** - * Builds a {@link ProtectionHooks} snapshot from the plugin's current hook state. - * - *

Extracted from {@link #dispatchChop} so that the hook wiring lives in one - * place and future hook additions only need to be made here. - */ private ProtectionHooks buildProtectionHooks() { + HookManager hm = plugin.getHookManager(); return new ProtectionHooks( - plugin.isWorldGuardEnabled(), - plugin.getWorldGuardHook(), - plugin.isResidenceEnabled(), - plugin.getResidenceHook(), - plugin.isGriefPreventionEnabled(), - plugin.getGriefPreventionHook(), - plugin.isLandsEnabled(), - plugin.getLandsHook()); + hm.isWorldGuardEnabled(), + hm.getWorldGuardHook(), + hm.isResidenceEnabled(), + hm.getResidenceHook(), + hm.isGriefPreventionEnabled(), + hm.getGriefPreventionHook(), + hm.isLandsEnabled(), + hm.getLandsHook()); } - /** - * Captures {@link ChunkSnapshot}s for all chunks within the leaf-detection radius. - * - *

Must be called on the main/region thread since it accesses live world state. - * Once captured, the returned snapshots are immutable and safe to read on any thread. - */ private Map captureLeafCheckSnapshots(Block log, int radius) { World world = log.getWorld(); int cx = log.getX(); @@ -248,14 +215,6 @@ private Map captureLeafCheckSnapshots(Block log, int radius return snapshots; } - /** - * Returns {@code true} if there is at least one leaf block within the configured - * detection radius centred on the given log. - * - *

Safe to call from an async thread — all block data is read from the - * pre-captured {@code snapshots}, which are immutable. Short-circuits on the - * first leaf found. - */ private static boolean hasNearbyLeaves(Block log, int radius, Config config, Map snapshots) { World world = log.getWorld(); int cx = log.getX(); diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java index ce59e01..b2305b9 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java @@ -45,10 +45,8 @@ public void onPlayerJoin(PlayerJoinEvent event) { .loadPlayerDataAsync(playerUUID, plugin.getPluginConfig().getDefaultTreeChop()) .thenAccept(data -> { PlayerConfig playerConfig = new PlayerConfig(playerUUID, data); - plugin.getAllPlayerConfigs().put(playerUUID, playerConfig); + plugin.getDataManager().getAllPlayerConfigs().put(playerUUID, playerConfig); - // markRejoin must be called here, after playerConfig is loaded, - // so we know whether ATC was enabled for this player. if (playerConfig.isAutoTreeChopEnabled()) { plugin.getConfirmationManager().markRejoin(playerUUID); } @@ -59,8 +57,7 @@ public void onPlayerJoin(PlayerJoinEvent event) { DatabaseManager.PlayerData defaultData = new DatabaseManager.PlayerData( playerUUID, plugin.getPluginConfig().getDefaultTreeChop(), 0, 0, java.time.LocalDate.now()); PlayerConfig fallback = new PlayerConfig(playerUUID, defaultData); - plugin.getAllPlayerConfigs().put(playerUUID, fallback); - // Default is disabled, so no markRejoin needed here. + plugin.getDataManager().getAllPlayerConfigs().put(playerUUID, fallback); return null; }); ModrinthUpdateChecker checker = plugin.getUpdateChecker(); diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java index 4fdcfe1..97c7204 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java @@ -39,15 +39,14 @@ public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); UUID playerUUID = player.getUniqueId(); - PlayerConfig playerConfig = plugin.getAllPlayerConfigs().get(playerUUID); + PlayerConfig playerConfig = + plugin.getDataManager().getAllPlayerConfigs().get(playerUUID); if (playerConfig != null && playerConfig.isDirty()) { plugin.getDatabaseManager().savePlayerDataSync(playerConfig.getData()); } - plugin.getAllPlayerConfigs().remove(playerUUID); + plugin.getDataManager().getAllPlayerConfigs().remove(playerUUID); SessionManager.getInstance().clearAllPlayerSessions(playerUUID); - - // Clear all confirmation state so memory doesn't leak between sessions. plugin.getConfirmationManager().clearPlayer(playerUUID); } } diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java index 7fc1c2a..83c0aa3 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java @@ -43,7 +43,7 @@ public void onPlayerToggleSneak(PlayerToggleSneakEvent event) { if (!player.hasPermission("autotreechop.use")) return; - PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(playerUUID); if (event.isSneaking()) { playerConfig.setAutoTreeChopEnabled(true); diff --git a/src/main/java/org/milkteamc/autotreechop/hooks/HookManager.java b/src/main/java/org/milkteamc/autotreechop/hooks/HookManager.java new file mode 100644 index 0000000..3c643f5 --- /dev/null +++ b/src/main/java/org/milkteamc/autotreechop/hooks/HookManager.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2026 MilkTeaMC and contributors + * + * 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 org.milkteamc.autotreechop.hooks; + +import org.bukkit.Bukkit; +import org.milkteamc.autotreechop.AutoTreeChop; +import org.milkteamc.autotreechop.Config; + +public class HookManager { + + private final AutoTreeChop plugin; + private WorldGuardHook worldGuardHook = null; + private ResidenceHook residenceHook = null; + private GriefPreventionHook griefPreventionHook = null; + private LandsHook landsHook = null; + + public HookManager(AutoTreeChop plugin, Config config) { + this.plugin = plugin; + initializeHooks(config); + } + + private void initializeHooks(Config config) { + if (Bukkit.getPluginManager().getPlugin("Residence") != null) { + try { + residenceHook = new ResidenceHook(config.getResidenceFlag()); + plugin.getLogger().info("Residence support enabled"); + } catch (Exception e) { + plugin.getLogger() + .warning( + "Residence can't be hooked, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); + } + } + + if (Bukkit.getPluginManager().getPlugin("GriefPrevention") != null) { + try { + griefPreventionHook = new GriefPreventionHook(config.getGriefPreventionFlag()); + plugin.getLogger().info("GriefPrevention support enabled"); + } catch (Exception e) { + plugin.getLogger() + .warning( + "GriefPrevention can't be hooked, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); + } + } + + if (Bukkit.getPluginManager().getPlugin("Lands") != null) { + try { + landsHook = new LandsHook(plugin); + plugin.getLogger().info("Lands support enabled"); + } catch (Exception e) { + plugin.getLogger() + .warning( + "Lands can't be hooked, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); + } + } + + if (Bukkit.getPluginManager().getPlugin("WorldGuard") != null) { + try { + worldGuardHook = new WorldGuardHook(); + plugin.getLogger().info("WorldGuard support enabled"); + } catch (NoClassDefFoundError e) { + plugin.getLogger() + .warning( + "WorldGuard can't be hooked, please report this to our GitHub: https://github.com/milkteamc/AutoTreeChop/issues"); + } + } + } + + public boolean isWorldGuardEnabled() { + return worldGuardHook != null; + } + + public boolean isResidenceEnabled() { + return residenceHook != null; + } + + public boolean isGriefPreventionEnabled() { + return griefPreventionHook != null; + } + + public boolean isLandsEnabled() { + return landsHook != null; + } + + public WorldGuardHook getWorldGuardHook() { + return worldGuardHook; + } + + public ResidenceHook getResidenceHook() { + return residenceHook; + } + + public GriefPreventionHook getGriefPreventionHook() { + return griefPreventionHook; + } + + public LandsHook getLandsHook() { + return landsHook; + } +} diff --git a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java index 40957a4..120b617 100644 --- a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java +++ b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java @@ -50,7 +50,7 @@ public void checkThreshold() { private int countDirtyData() { int count = 0; - for (PlayerConfig config : plugin.getAllPlayerConfigs().values()) { + for (PlayerConfig config : plugin.getDataManager().getAllPlayerConfigs().values()) { if (config.isDirty()) { count++; } @@ -61,7 +61,8 @@ private int countDirtyData() { private void saveAllDirtyData() { Map dirtyDataMap = new HashMap<>(); - for (Map.Entry entry : plugin.getAllPlayerConfigs().entrySet()) { + for (Map.Entry entry : + plugin.getDataManager().getAllPlayerConfigs().entrySet()) { PlayerConfig config = entry.getValue(); if (config.isDirty()) { dirtyDataMap.put(entry.getKey(), config.getData()); @@ -78,7 +79,9 @@ private void saveAllDirtyData() { .exceptionally(ex -> { plugin.getLogger().warning("Failed to save player data: " + ex.getMessage()); for (UUID uuid : dirtyDataMap.keySet()) { - PlayerConfig config = plugin.getPlayerConfig(uuid); + PlayerConfig config = plugin.getDataManager() + .getAllPlayerConfigs() + .get(uuid); if (config != null) { config.markDirty(); } From 8e84a0f6efb67c6d6bd10cc0cd5208dd1fbd8c69 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sun, 12 Apr 2026 09:20:43 +0800 Subject: [PATCH 3/5] fix some more small issues --- .../autotreechop/database/DataManager.java | 18 +++++++++--- .../database/DatabaseManager.java | 8 +++++ .../events/BlockBreakListener.java | 29 +++++++++---------- .../events/PlayerJoinListener.java | 23 +++++++++++---- .../events/PlayerQuitListener.java | 10 +++---- .../tasks/PlayerDataSaveTask.java | 14 ++++----- .../autotreechop/utils/SessionManager.java | 11 +++++++ 7 files changed, 74 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/database/DataManager.java b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java index 88344b0..2e81c85 100644 --- a/src/main/java/org/milkteamc/autotreechop/database/DataManager.java +++ b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java @@ -19,6 +19,8 @@ import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -99,6 +101,14 @@ public void shutdown() { } } + public void addPlayerConfig(UUID uuid, PlayerConfig config) { + this.playerConfigs.put(uuid, config); + } + + public void removePlayerConfig(UUID uuid) { + this.playerConfigs.remove(uuid); + } + public PlayerConfig getPlayerConfig(UUID playerUUID) { return playerConfigs.computeIfAbsent(playerUUID, k -> { DatabaseManager.PlayerData tempDefaultData = @@ -107,6 +117,10 @@ public PlayerConfig getPlayerConfig(UUID playerUUID) { }); } + public Collection getOnlinePlayersConfigs() { + return Collections.unmodifiableCollection(playerConfigs.values()); + } + public int getPlayerDailyUses(UUID playerUUID) { return getPlayerConfig(playerUUID).getDailyUses(); } @@ -114,8 +128,4 @@ public int getPlayerDailyUses(UUID playerUUID) { public int getPlayerDailyBlocksBroken(UUID playerUUID) { return getPlayerConfig(playerUUID).getDailyBlocksBroken(); } - - public Map getAllPlayerConfigs() { - return playerConfigs; - } } diff --git a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java index 818ddc1..a7d3001 100644 --- a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java +++ b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java @@ -234,6 +234,14 @@ public PlayerData( this.lastUseDate = lastUseDate; } + public PlayerData(PlayerData source) { + this.playerUUID = source.playerUUID; + this.autoTreeChopEnabled = source.autoTreeChopEnabled; + this.dailyUses = source.dailyUses; + this.dailyBlocksBroken = source.dailyBlocksBroken; + this.lastUseDate = source.lastUseDate; + } + public UUID getPlayerUUID() { return playerUUID; } diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 7d1e114..746e424 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -19,9 +19,7 @@ import java.util.HashMap; import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import org.bukkit.ChunkSnapshot; import org.bukkit.Location; @@ -54,8 +52,6 @@ public class BlockBreakListener implements Listener { private final AutoTreeChop plugin; private final AsyncTaskScheduler scheduler; - private final Set leafCheckInProgress = ConcurrentHashMap.newKeySet(); - public BlockBreakListener(AutoTreeChop plugin) { this.plugin = plugin; this.scheduler = new AsyncTaskScheduler(plugin); @@ -87,6 +83,17 @@ public void onBlockBreak(BlockBreakEvent event) { return; } + ConfirmationManager confirmationManager = plugin.getConfirmationManager(); + ChopData pending = confirmationManager.consumePendingConfirmation(playerUUID); + + if (pending != null) { + event.setCancelled(true); + confirmationManager.recordSuccessfulChop(playerUUID, pending.reason(), false); + AutoTreeChop.sendMessage(player, MessageKeys.CONFIRMATION_SUCCESS); + dispatchChop(player, playerConfig, block, tool, location, config); + return; + } + if (plugin.getCooldownManager().isInCooldown(playerUUID)) { long remaining = plugin.getCooldownManager().getRemainingCooldown(playerUUID); AutoTreeChop.sendMessage( @@ -108,19 +115,9 @@ public void onBlockBreak(BlockBreakEvent event) { return; } - ConfirmationManager confirmationManager = plugin.getConfirmationManager(); - ChopData pending = confirmationManager.consumePendingConfirmation(playerUUID); - event.setCancelled(true); - if (pending != null) { - confirmationManager.recordSuccessfulChop(playerUUID, pending.reason(), false); - AutoTreeChop.sendMessage(player, MessageKeys.CONFIRMATION_SUCCESS); - dispatchChop(player, playerConfig, block, tool, location, config); - return; - } - - if (!leafCheckInProgress.add(playerUUID)) { + if (!SessionManager.getInstance().startLeafCheck(playerUUID)) { return; } @@ -156,7 +153,7 @@ public void onBlockBreak(BlockBreakEvent event) { confirmationManager.recordSuccessfulChop(playerUUID, null, hasLeaves); dispatchChop(player, playerConfig, block, frozenTool, frozenLocation, config); } finally { - leafCheckInProgress.remove(playerUUID); + SessionManager.getInstance().finishLeafCheck(playerUUID); } }); }); diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java index b2305b9..83e1e39 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java @@ -44,8 +44,13 @@ public void onPlayerJoin(PlayerJoinEvent event) { plugin.getDatabaseManager() .loadPlayerDataAsync(playerUUID, plugin.getPluginConfig().getDefaultTreeChop()) .thenAccept(data -> { + Player onlinePlayer = plugin.getServer().getPlayer(playerUUID); + if (onlinePlayer == null || !onlinePlayer.isOnline()) { + return; + } + PlayerConfig playerConfig = new PlayerConfig(playerUUID, data); - plugin.getDataManager().getAllPlayerConfigs().put(playerUUID, playerConfig); + plugin.getDataManager().addPlayerConfig(playerUUID, playerConfig); if (playerConfig.isAutoTreeChopEnabled()) { plugin.getConfirmationManager().markRejoin(playerUUID); @@ -54,10 +59,18 @@ public void onPlayerJoin(PlayerJoinEvent event) { .exceptionally(ex -> { plugin.getLogger() .warning("Failed to load data for player " + player.getName() + ": " + ex.getMessage()); - DatabaseManager.PlayerData defaultData = new DatabaseManager.PlayerData( - playerUUID, plugin.getPluginConfig().getDefaultTreeChop(), 0, 0, java.time.LocalDate.now()); - PlayerConfig fallback = new PlayerConfig(playerUUID, defaultData); - plugin.getDataManager().getAllPlayerConfigs().put(playerUUID, fallback); + + Player onlinePlayer = plugin.getServer().getPlayer(playerUUID); + if (onlinePlayer != null && onlinePlayer.isOnline()) { + DatabaseManager.PlayerData defaultData = new DatabaseManager.PlayerData( + playerUUID, + plugin.getPluginConfig().getDefaultTreeChop(), + 0, + 0, + java.time.LocalDate.now()); + PlayerConfig fallback = new PlayerConfig(playerUUID, defaultData); + plugin.getDataManager().addPlayerConfig(playerUUID, fallback); + } return null; }); ModrinthUpdateChecker checker = plugin.getUpdateChecker(); diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java index 97c7204..a745358 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java @@ -18,7 +18,6 @@ package org.milkteamc.autotreechop.events; import java.util.UUID; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; @@ -36,17 +35,16 @@ public PlayerQuitListener(AutoTreeChop plugin) { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - UUID playerUUID = player.getUniqueId(); + UUID playerUUID = event.getPlayer().getUniqueId(); + PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(playerUUID); - PlayerConfig playerConfig = - plugin.getDataManager().getAllPlayerConfigs().get(playerUUID); if (playerConfig != null && playerConfig.isDirty()) { plugin.getDatabaseManager().savePlayerDataSync(playerConfig.getData()); } - plugin.getDataManager().getAllPlayerConfigs().remove(playerUUID); + plugin.getDataManager().removePlayerConfig(playerUUID); SessionManager.getInstance().clearAllPlayerSessions(playerUUID); + SessionManager.getInstance().finishLeafCheck(playerUUID); plugin.getConfirmationManager().clearPlayer(playerUUID); } } diff --git a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java index 120b617..eb761db 100644 --- a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java +++ b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java @@ -50,7 +50,7 @@ public void checkThreshold() { private int countDirtyData() { int count = 0; - for (PlayerConfig config : plugin.getDataManager().getAllPlayerConfigs().values()) { + for (PlayerConfig config : plugin.getDataManager().getOnlinePlayersConfigs()) { if (config.isDirty()) { count++; } @@ -61,11 +61,11 @@ private int countDirtyData() { private void saveAllDirtyData() { Map dirtyDataMap = new HashMap<>(); - for (Map.Entry entry : - plugin.getDataManager().getAllPlayerConfigs().entrySet()) { - PlayerConfig config = entry.getValue(); + for (PlayerConfig config : plugin.getDataManager().getOnlinePlayersConfigs()) { if (config.isDirty()) { - dirtyDataMap.put(entry.getKey(), config.getData()); + DatabaseManager.PlayerData snapshot = new DatabaseManager.PlayerData(config.getData()); + + dirtyDataMap.put(snapshot.getPlayerUUID(), snapshot); config.clearDirty(); } } @@ -79,9 +79,7 @@ private void saveAllDirtyData() { .exceptionally(ex -> { plugin.getLogger().warning("Failed to save player data: " + ex.getMessage()); for (UUID uuid : dirtyDataMap.keySet()) { - PlayerConfig config = plugin.getDataManager() - .getAllPlayerConfigs() - .get(uuid); + PlayerConfig config = plugin.getDataManager().getPlayerConfig(uuid); if (config != null) { config.markDirty(); } diff --git a/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java b/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java index e3f758c..aa2ec0d 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java @@ -34,6 +34,7 @@ public class SessionManager { private final Map> treeChopProcessingLocations = new ConcurrentHashMap<>(); private final Map> leafRemovalRemovedLogs = new ConcurrentHashMap<>(); private final Set activeLeafRemovalSessions = ConcurrentHashMap.newKeySet(); + private final Set leafCheckInProgress = ConcurrentHashMap.newKeySet(); private SessionManager() {} @@ -152,6 +153,14 @@ public boolean isLogRemoved(String sessionId, Location location) { && Objects.equals(loc.getWorld(), location.getWorld())); } + public boolean startLeafCheck(UUID uuid) { + return leafCheckInProgress.add(uuid); + } + + public void finishLeafCheck(UUID uuid) { + leafCheckInProgress.remove(uuid); + } + /** * End a leaf removal session and cleanup */ @@ -174,6 +183,8 @@ public boolean hasAnyActiveSession(UUID playerUUID) { public void clearAllPlayerSessions(UUID playerUUID) { clearTreeChopSession(playerUUID); + finishLeafCheck(playerUUID); + String playerKey = playerUUID.toString(); // Find and remove all leaf removal sessions for this player List toRemove = new ArrayList<>(); From da6c369da50137983c4297c415210b336e60a88e Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sun, 12 Apr 2026 12:57:24 +0800 Subject: [PATCH 4/5] improve database --- .../milkteamc/autotreechop/PlayerConfig.java | 36 ++++++++++--------- .../autotreechop/database/DataManager.java | 35 +++++++++--------- .../events/PlayerQuitListener.java | 20 ++++++++--- .../tasks/PlayerDataSaveTask.java | 31 +++++++--------- 4 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java b/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java index 25808c3..c964c7b 100644 --- a/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java +++ b/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java @@ -37,56 +37,60 @@ private void checkAndUpdateDate() { data.setDailyUses(0); data.setDailyBlocksBroken(0); data.setLastUseDate(LocalDate.now()); - markDirty(); + this.dirty = true; } } - public boolean isAutoTreeChopEnabled() { + public synchronized boolean isAutoTreeChopEnabled() { return data.isAutoTreeChopEnabled(); } - public void setAutoTreeChopEnabled(boolean enabled) { + public synchronized void setAutoTreeChopEnabled(boolean enabled) { if (data.isAutoTreeChopEnabled() != enabled) { data.setAutoTreeChopEnabled(enabled); - markDirty(); + this.dirty = true; } } - public int getDailyUses() { + public synchronized int getDailyUses() { checkAndUpdateDate(); return data.getDailyUses(); } - public void incrementDailyUses() { + public synchronized void incrementDailyUses() { checkAndUpdateDate(); data.incrementDailyUses(); - markDirty(); + this.dirty = true; } - public int getDailyBlocksBroken() { + public synchronized int getDailyBlocksBroken() { checkAndUpdateDate(); return data.getDailyBlocksBroken(); } - public void incrementDailyBlocksBroken() { + public synchronized void incrementDailyBlocksBroken() { checkAndUpdateDate(); data.incrementDailyBlocksBroken(); - markDirty(); + this.dirty = true; } - public void markDirty() { + public synchronized void markDirty() { this.dirty = true; } - public void clearDirty() { - this.dirty = false; + public synchronized boolean isDirty() { + return dirty; } - public boolean isDirty() { - return dirty; + public synchronized DatabaseManager.PlayerData popSnapshotIfDirty() { + if (this.dirty) { + this.dirty = false; + return new DatabaseManager.PlayerData(this.data); + } + return null; } - public DatabaseManager.PlayerData getData() { + public synchronized DatabaseManager.PlayerData getData() { return data; } diff --git a/src/main/java/org/milkteamc/autotreechop/database/DataManager.java b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java index 2e81c85..c0c7d7e 100644 --- a/src/main/java/org/milkteamc/autotreechop/database/DataManager.java +++ b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java @@ -20,7 +20,6 @@ import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -77,20 +76,22 @@ public void shutdown() { confirmationManager.clearPlayer(uuid); } - if (pConfig.isDirty()) { - dirtyDataList.add(pConfig.getData()); - } - if (sessionManager != null) { sessionManager.clearAllPlayerSessions(uuid); } + + DatabaseManager.PlayerData snapshot = pConfig.popSnapshotIfDirty(); + if (snapshot != null) { + dirtyDataList.add(snapshot); + } } if (!dirtyDataList.isEmpty() && databaseManager != null) { long startTime = System.currentTimeMillis(); databaseManager.savePlayerDataBatchSync(dirtyDataList); long duration = System.currentTimeMillis() - startTime; - plugin.getLogger().info("Successfully saved player records in " + duration + "ms."); + plugin.getLogger() + .info("Successfully saved " + dirtyDataList.size() + " player records in " + duration + "ms."); } playerConfigs.clear(); @@ -102,30 +103,28 @@ public void shutdown() { } public void addPlayerConfig(UUID uuid, PlayerConfig config) { - this.playerConfigs.put(uuid, config); + playerConfigs.put(uuid, config); } - public void removePlayerConfig(UUID uuid) { - this.playerConfigs.remove(uuid); + public PlayerConfig removePlayerConfig(UUID uuid) { + return playerConfigs.remove(uuid); } - public PlayerConfig getPlayerConfig(UUID playerUUID) { - return playerConfigs.computeIfAbsent(playerUUID, k -> { - DatabaseManager.PlayerData tempDefaultData = - new DatabaseManager.PlayerData(k, false, 0, 0, java.time.LocalDate.now()); - return new PlayerConfig(k, tempDefaultData); - }); + public PlayerConfig getPlayerConfig(UUID uuid) { + return playerConfigs.get(uuid); } public Collection getOnlinePlayersConfigs() { - return Collections.unmodifiableCollection(playerConfigs.values()); + return playerConfigs.values(); } public int getPlayerDailyUses(UUID playerUUID) { - return getPlayerConfig(playerUUID).getDailyUses(); + PlayerConfig config = getPlayerConfig(playerUUID); + return config != null ? config.getDailyUses() : 0; } public int getPlayerDailyBlocksBroken(UUID playerUUID) { - return getPlayerConfig(playerUUID).getDailyBlocksBroken(); + PlayerConfig config = getPlayerConfig(playerUUID); + return config != null ? config.getDailyBlocksBroken() : 0; } } diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java index a745358..73e6e87 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java @@ -17,12 +17,14 @@ package org.milkteamc.autotreechop.events; +import java.util.Map; import java.util.UUID; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; import org.milkteamc.autotreechop.AutoTreeChop; import org.milkteamc.autotreechop.PlayerConfig; +import org.milkteamc.autotreechop.database.DatabaseManager; import org.milkteamc.autotreechop.utils.SessionManager; public class PlayerQuitListener implements Listener { @@ -36,13 +38,23 @@ public PlayerQuitListener(AutoTreeChop plugin) { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { UUID playerUUID = event.getPlayer().getUniqueId(); - PlayerConfig playerConfig = plugin.getDataManager().getPlayerConfig(playerUUID); - if (playerConfig != null && playerConfig.isDirty()) { - plugin.getDatabaseManager().savePlayerDataSync(playerConfig.getData()); + PlayerConfig playerConfig = plugin.getDataManager().removePlayerConfig(playerUUID); + + if (playerConfig != null) { + DatabaseManager.PlayerData snapshot = playerConfig.popSnapshotIfDirty(); + if (snapshot != null) { + plugin.getDatabaseManager() + .savePlayerDataBatchAsync(Map.of(playerUUID, snapshot)) + .exceptionally(ex -> { + plugin.getLogger() + .warning("Failed to save final data for quitting player " + playerUUID + ": " + + ex.getMessage()); + return null; + }); + } } - plugin.getDataManager().removePlayerConfig(playerUUID); SessionManager.getInstance().clearAllPlayerSessions(playerUUID); SessionManager.getInstance().finishLeafCheck(playerUUID); plugin.getConfirmationManager().clearPlayer(playerUUID); diff --git a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java index eb761db..d90fdb3 100644 --- a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java +++ b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java @@ -62,30 +62,23 @@ private void saveAllDirtyData() { Map dirtyDataMap = new HashMap<>(); for (PlayerConfig config : plugin.getDataManager().getOnlinePlayersConfigs()) { - if (config.isDirty()) { - DatabaseManager.PlayerData snapshot = new DatabaseManager.PlayerData(config.getData()); - + DatabaseManager.PlayerData snapshot = config.popSnapshotIfDirty(); + if (snapshot != null) { dirtyDataMap.put(snapshot.getPlayerUUID(), snapshot); - config.clearDirty(); } } if (!dirtyDataMap.isEmpty()) { - plugin.getDatabaseManager() - .savePlayerDataBatchAsync(dirtyDataMap) - .thenRun(() -> { - dirtyCount = 0; - }) - .exceptionally(ex -> { - plugin.getLogger().warning("Failed to save player data: " + ex.getMessage()); - for (UUID uuid : dirtyDataMap.keySet()) { - PlayerConfig config = plugin.getDataManager().getPlayerConfig(uuid); - if (config != null) { - config.markDirty(); - } - } - return null; - }); + plugin.getDatabaseManager().savePlayerDataBatchAsync(dirtyDataMap).exceptionally(ex -> { + plugin.getLogger().warning("Failed to save player data: " + ex.getMessage()); + for (UUID uuid : dirtyDataMap.keySet()) { + PlayerConfig config = plugin.getDataManager().getPlayerConfig(uuid); + if (config != null) { + config.markDirty(); + } + } + return null; + }); } } } From b178a1abb7046bb9d0dca589934600bc97ba6b65 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sun, 12 Apr 2026 13:17:59 +0800 Subject: [PATCH 5/5] some more improvement --- .../events/BlockBreakListener.java | 15 +++++++++------ .../autotreechop/utils/TreeChopUtils.java | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 746e424..9fd9968 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -199,14 +199,17 @@ private Map captureLeafCheckSnapshots(Block log, int radius int cz = log.getZ(); Map snapshots = new HashMap<>(); - for (int dx = -radius; dx <= radius; dx++) { - for (int dz = -radius; dz <= radius; dz++) { - int chunkX = (cx + dx) >> 4; - int chunkZ = (cz + dz) >> 4; + int minChunkX = (cx - radius) >> 4; + int maxChunkX = (cx + radius) >> 4; + int minChunkZ = (cz - radius) >> 4; + int maxChunkZ = (cz + radius) >> 4; + + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { if (!world.isChunkLoaded(chunkX, chunkZ)) continue; + long key = chunkKey(chunkX, chunkZ); - snapshots.computeIfAbsent( - key, k -> world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(false, false, false)); + snapshots.put(key, world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(false, false, false)); } } return snapshots; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 0d5d94c..2f85acb 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -95,11 +95,18 @@ private static void applyToolDamage(ItemStack tool, Player player, int blocksBro } } + if (damageToApply == 0) return; + int currentDamage = damageableMeta.getDamage(); int newDamage = currentDamage + damageToApply; if (newDamage >= tool.getType().getMaxDurability()) { - player.getInventory().setItemInMainHand(null); + tool.setAmount(0); + + try { + XSound.ENTITY_ITEM_BREAK.play(player.getLocation(), 1.0f, 1.0f); + } catch (Exception ignored) { + } } else { damageableMeta.setDamage(newDamage); tool.setItemMeta(damageableMeta); @@ -123,11 +130,16 @@ private static boolean shouldApplyDurabilityLoss(int unbreakingLevel, Config con public static boolean isTool(Player player) { ItemStack item = player.getInventory().getItemInMainHand(); - if (item == null || XMaterial.matchXMaterial(item) == XMaterial.AIR) { + if (item == null) { return false; } - String materialName = item.getType().toString(); + XMaterial xMat = XMaterial.matchXMaterial(item); + if (xMat == XMaterial.AIR) { + return false; + } + + String materialName = xMat.name(); if (materialName.endsWith("_AXE") || materialName.endsWith("_HOE") @@ -137,7 +149,6 @@ public static boolean isTool(Player player) { return true; } - XMaterial xMat = XMaterial.matchXMaterial(item); return xMat == XMaterial.SHEARS || xMat == XMaterial.FISHING_ROD || xMat == XMaterial.FLINT_AND_STEEL; }