diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 531a060..097b1e5 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -17,12 +17,8 @@ package org.milkteamc.autotreechop; -import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.io.File; 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; @@ -34,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 { @@ -102,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") @@ -114,137 +89,51 @@ public void onEnable() { instance = this; saveDefaultConfig(); - config = new Config(this); + this.config = new Config(this); + setupTranslation(); - 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)); - - 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()); } @@ -255,147 +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(); - for (Map.Entry entry : playerConfigs.entrySet()) { - UUID uuid = entry.getKey(); - PlayerConfig pConfig = entry.getValue(); - - if (confirmationManager != null) { - confirmationManager.clearPlayer(uuid); - } - - if (pConfig.isDirty() && databaseManager != null) { - databaseManager.savePlayerDataSync(pConfig.getData()); - } - - if (sessionManager != null) { - sessionManager.clearAllPlayerSessions(uuid); - } - } - 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 HookManager getHookManager() { + return hookManager; } - public boolean isResidenceEnabled() { - return residenceEnabled; - } - - 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/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/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..c0c7d7e --- /dev/null +++ b/src/main/java/org/milkteamc/autotreechop/database/DataManager.java @@ -0,0 +1,130 @@ +/* + * 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.Collection; +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 (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 " + dirtyDataList.size() + " player records in " + duration + "ms."); + } + + playerConfigs.clear(); + } + + if (databaseManager != null) { + databaseManager.close(); + } + } + + public void addPlayerConfig(UUID uuid, PlayerConfig config) { + playerConfigs.put(uuid, config); + } + + public PlayerConfig removePlayerConfig(UUID uuid) { + return playerConfigs.remove(uuid); + } + + public PlayerConfig getPlayerConfig(UUID uuid) { + return playerConfigs.get(uuid); + } + + public Collection getOnlinePlayersConfigs() { + return playerConfigs.values(); + } + + public int getPlayerDailyUses(UUID playerUUID) { + PlayerConfig config = getPlayerConfig(playerUUID); + return config != null ? config.getDailyUses() : 0; + } + + public int getPlayerDailyBlocksBroken(UUID playerUUID) { + PlayerConfig config = getPlayerConfig(playerUUID); + return config != null ? config.getDailyBlocksBroken() : 0; + } +} diff --git a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java index c4a37f2..a7d3001 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(); - String sql = buildUpsertSql(); - try (Connection conn = dataSource.getConnection()) { - conn.setAutoCommit(false); + try (Connection conn = dataSource.getConnection()) { + boolean originalAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); - 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 (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() { @@ -224,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 12c6d9e..9fd9968 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; @@ -38,6 +36,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,18 +52,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) { this.plugin = plugin; this.scheduler = new AsyncTaskScheduler(plugin); @@ -74,7 +61,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(); @@ -96,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( @@ -117,51 +115,28 @@ 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)) { + if (!SessionManager.getInstance().startLeafCheck(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()); @@ -178,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); } }); }); @@ -205,57 +180,41 @@ 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(); 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; } - /** - * 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..83e1e39 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java @@ -44,11 +44,14 @@ 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.getAllPlayerConfigs().put(playerUUID, playerConfig); + plugin.getDataManager().addPlayerConfig(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); } @@ -56,11 +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.getAllPlayerConfigs().put(playerUUID, fallback); - // Default is disabled, so no markRejoin needed here. + + 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 4fdcfe1..73e6e87 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java @@ -17,13 +17,14 @@ package org.milkteamc.autotreechop.events; +import java.util.Map; 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; 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,18 +37,26 @@ public PlayerQuitListener(AutoTreeChop plugin) { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - UUID playerUUID = player.getUniqueId(); - - PlayerConfig playerConfig = plugin.getAllPlayerConfigs().get(playerUUID); - if (playerConfig != null && playerConfig.isDirty()) { - plugin.getDatabaseManager().savePlayerDataSync(playerConfig.getData()); + UUID playerUUID = event.getPlayer().getUniqueId(); + + 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.getAllPlayerConfigs().remove(playerUUID); SessionManager.getInstance().clearAllPlayerSessions(playerUUID); - - // Clear all confirmation state so memory doesn't leak between sessions. + SessionManager.getInstance().finishLeafCheck(playerUUID); 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..d90fdb3 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().getOnlinePlayersConfigs()) { if (config.isDirty()) { count++; } @@ -61,30 +61,24 @@ private int countDirtyData() { private void saveAllDirtyData() { Map dirtyDataMap = new HashMap<>(); - for (Map.Entry entry : plugin.getAllPlayerConfigs().entrySet()) { - PlayerConfig config = entry.getValue(); - if (config.isDirty()) { - dirtyDataMap.put(entry.getKey(), config.getData()); - config.clearDirty(); + for (PlayerConfig config : plugin.getDataManager().getOnlinePlayersConfigs()) { + DatabaseManager.PlayerData snapshot = config.popSnapshotIfDirty(); + if (snapshot != null) { + dirtyDataMap.put(snapshot.getPlayerUUID(), snapshot); } } 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.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; + }); } } } 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<>(); 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; }