From 74ee9d829d20070c29841483209efccd73df23b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:28:42 +0000 Subject: [PATCH 01/55] Bump me.clip:placeholderapi from 2.11.7 to 2.12.2 Bumps me.clip:placeholderapi from 2.11.7 to 2.12.2. --- updated-dependencies: - dependency-name: me.clip:placeholderapi dependency-version: 2.12.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 70177db..26be7c9 100644 --- a/build.gradle +++ b/build.gradle @@ -84,7 +84,7 @@ dependencies { implementation 'com.github.cryptomorin:XSeries:13.6.0' implementation 'dev.dejvokep:boosted-yaml:1.3.7' - compileOnly 'me.clip:placeholderapi:2.11.7' + compileOnly 'me.clip:placeholderapi:2.12.2' compileOnly files('./libs/Residence5.1.4.3.jar') compileOnly 'com.github.angeschossen:LandsAPI:7.17.2' compileOnly 'com.sk89q.worldguard:worldguard-bukkit:7.0.14' From 1b933423a229073e012c9fe65dd242087a1bfe9f Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Tue, 3 Mar 2026 16:52:07 +0800 Subject: [PATCH 02/55] fix grammer --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2781dac..854cfbf 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ It's async-friendly, lightweight, and fully customizable — with built-in suppo | `/atc usage` | Show daily usage | | `/atc reload` | Reload plugin config | | `/atc toggle ` | Toggle for another player | -| `/atc enable ` | Enable for another players | -| `/atc disable ` | Disable for another players | +| `/atc enable ` | Enable for other players | +| `/atc disable ` | Disable for other players | | `/atc about` | Plugin info | --- From bcdd4d43055c24517f9f94d6c4a0b51479a182cd Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Tue, 3 Mar 2026 16:53:07 +0800 Subject: [PATCH 03/55] bump lamp version --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 70177db..7869255 100644 --- a/build.gradle +++ b/build.gradle @@ -75,8 +75,8 @@ dependencies { implementation 'net.kyori:adventure-platform-bukkit:4.4.1' implementation 'net.kyori:adventure-text-minimessage:4.26.1' - implementation 'io.github.revxrsal:lamp.common:4.0.0-rc.14' - implementation 'io.github.revxrsal:lamp.bukkit:4.0.0-rc.14' + implementation 'io.github.revxrsal:lamp.common:4.0.0-rc.16' + implementation 'io.github.revxrsal:lamp.bukkit:4.0.0-rc.16' implementation 'com.github.Anon8281:UniversalScheduler:0.1.7' implementation 'com.zaxxer:HikariCP:7.0.2' From fedcf890cef961c33578dda0c1b751d5e889a83b Mon Sep 17 00:00:00 2001 From: anovilif Date: Wed, 4 Mar 2026 10:16:28 +0000 Subject: [PATCH 04/55] Added translation using Weblate (Italian) --- src/main/resources/lang/it.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/lang/it.properties diff --git a/src/main/resources/lang/it.properties b/src/main/resources/lang/it.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/lang/it.properties @@ -0,0 +1 @@ + From eb825f3da82b2617fe148331dba84480d1349f78 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 4 Mar 2026 12:28:26 +0000 Subject: [PATCH 05/55] Translated using Weblate (Spanish) Currently translated at 100.0% (27 of 27 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/es/ --- src/main/resources/lang/es.properties | 39 ++++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main/resources/lang/es.properties b/src/main/resources/lang/es.properties index e2bba6d..9dfa37e 100644 --- a/src/main/resources/lang/es.properties +++ b/src/main/resources/lang/es.properties @@ -1,14 +1,27 @@ -blocks-broken=Has talado {current_blocks}/{max_blocks} bloques hoy. +blocks-broken=Bloques rotos hoy: {current_blocks}/{max_blocks} consoleName=Consola -disabled=Tala automática desactivada. -disabledByOther=Tala automática desactivada por {player}. -disabledForOther=Tala automática desactivada para {player} -enabled=Tala automática activada. -enabledByOther=Tala automática activada por {player} -enabledForOther=Tala automática activada para {player} -hitmaxblock=Has alcanzado tu límite diario de bloques para la tala automática. -hitmaxusage=Has alcanzado tu límite diario de uso de la tala automática. -no-permission=No tienes permiso para hacer eso. -usage=Has usado la tala automática {current_uses}/{max_uses} veces hoy. -noResidencePermissions=No tienes permiso para usar la tala automática aquí. -stillInCooldown=¡Todavía estás en enfriamiento! Inténtalo de nuevo después de {cooldown_time} segundos. \ No newline at end of file +disabled=AutoTreeChop desactivado. +disabledByOther=AutoTreeChop fue desactivado por {player}. +disabledForOther=AutoTreeChop fue desactivado para {player}. +enabled=AutoTreeChop habilitado. +enabledByOther=AutoTreeChop fue activado por {player}. +enabledForOther=AutoTreeChop se activó para {player}. +hitmaxblock=Has alcanzado tu límite diario de ruptura de bloques. +hitmaxusage=Has alcanzado tu límite de uso diario. +no-permission=No tienes permiso para realizar esta acción. +usage=Daily AutoTreeChop usó: {current_uses}/{max_uses} +noResidencePermissions=No tienes permiso para usar AutoTreeChop aquí. +stillInCooldown=AutoTreeChop se está enfriando. Por favor, espere {cooldown_time} segundos. +only-players=Este comando solo puede ser utilizado por jugadores. +confirmationRequiredIdle=AutoTreeChop no se ha utilizado recientemente. Recorte un registro de nuevo (o /atc confirm) dentro de {timeout}s para confirmar. +confirmationRequiredNoLeaves=No se detectó ninguna hoja cercana. Este registro puede estar colocado por el jugador. Cortar otra vez (o /atc confirm) dentro de {timeout}s para confirmar. +confirmationRequiredBoth=AutoTreeChop no se ha utilizado recientemente y no se han detectado hojas cercanas. Cortar otra vez (o /atc confirm) dentro de {timeout}s para confirmar. +confirmationSuccess=Confirmación exitosa. AutoTreeChop ahora está activo. +noPendingConfirmation=No hay ninguna confirmación pendiente de AutoTreeChop. +sneakEnabled=Se habilita la opción de seguimiento automático mientras se oculta. +sneakDisabled=La opción de hacer clic automáticamente se deshabilitó después de parar. +aboutHeader=AutoTreeChop - v{versión} por el equipo de MilkTeaMC y los colaboradores +aboutLicense=Licencia: GNU General Public License v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutDiscord=Discord: https://discord.gg/uQ4UXANnP2 +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop From 0b7e305973c6f1a25fcb312c00ef1231304f875a Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Thu, 5 Mar 2026 19:19:20 +0800 Subject: [PATCH 06/55] fix some possibly small issues --- .../java/org/milkteamc/autotreechop/AutoTreeChop.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 9d3e41d..91945c2 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -122,6 +122,10 @@ public void onEnable() { // 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)); @@ -130,10 +134,6 @@ public void onEnable() { lamp.register(new UsageCommand(this, config)); lamp.register(new ConfirmCommand(this)); - // Initialize translation system - translationManager = new TranslationManager(this); - loadLocale(); - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { new AutoTreeChopExpansion(this).register(); getLogger().info("PlaceholderAPI expansion for AutoTreeChop has been registered."); From 63bdda3ee9cfbc20e6f3c11a9afbf2a449b46683 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Thu, 5 Mar 2026 19:41:43 +0800 Subject: [PATCH 07/55] fix #85 --- .../autotreechop/utils/TreeChopUtils.java | 3 +- .../autotreechop/utils/TreeReplantUtils.java | 69 +++++++++++++++++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 3af15ff..1494a5c 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -360,7 +360,8 @@ private void executeTreeChop( hooks.lands, hooks.residence, hooks.griefPrevention, - hooks.worldGuard); + hooks.worldGuard, + actuallyRemovedLogs); } } diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java index 65a4cac..57cb190 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java @@ -1,9 +1,11 @@ package org.milkteamc.autotreechop.utils; import com.cryptomorin.xseries.XMaterial; +import java.util.Set; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Player; @@ -33,7 +35,8 @@ public static void scheduleReplant( LandsHook landsHook, ResidenceHook residenceHook, GriefPreventionHook griefPreventionHook, - WorldGuardHook worldGuardHook) { + WorldGuardHook worldGuardHook, + Set choppedLogs) { if (!config.isAutoReplantEnabled()) { return; @@ -45,7 +48,7 @@ public static void scheduleReplant( } Location originalLocation = brokenLogBlock.getLocation().clone(); - boolean needs2x2 = requires2x2Formation(originalLogType); + boolean needs2x2 = isLikely2x2Tree(originalLogType, originalLocation, choppedLogs); Runnable replantTask = () -> { if (needs2x2) { @@ -110,11 +113,67 @@ public static void scheduleReplant( } /** - * Returns true for tree types that require a 2x2 sapling formation to grow. + * Determines whether the chopped tree should be replanted as a 2x2 sapling + * formation. + * + *

Dark Oak and Pale Oak are always 2x2. Spruce and Jungle are 2x2 only when + * the base of the chopped tree contained four logs arranged in a 2x2 square — + * detected by scanning the chopped-log set for a matching pattern at the Y + * level of the lowest broken log. All other tree types are always single. */ - private static boolean requires2x2Formation(Material logType) { + private static boolean isLikely2x2Tree(Material logType, Location lowestLogLocation, Set choppedLogs) { + XMaterial xMat = XMaterial.matchXMaterial(logType); - return xMat == XMaterial.DARK_OAK_LOG || xMat == XMaterial.PALE_OAK_LOG; + + // Dark Oak and Pale Oak are always planted as 2x2 + if (xMat == XMaterial.DARK_OAK_LOG || xMat == XMaterial.PALE_OAK_LOG) { + return true; + } + + // Only Spruce and Jungle can be big (2x2) trees — everything else is always single + if (xMat != XMaterial.SPRUCE_LOG && xMat != XMaterial.JUNGLE_LOG) { + return false; + } + + // Detect 2x2 by checking whether four logs of this type form a square at + // the base Y level among the actually-chopped blocks. + int baseY = lowestLogLocation.getBlockY(); + int baseX = lowestLogLocation.getBlockX(); + int baseZ = lowestLogLocation.getBlockZ(); + World world = lowestLogLocation.getWorld(); + + // Try all four possible 2x2 anchors that include the base-log position as a corner + int[][] candidateAnchors = {{0, 0}, {-1, 0}, {0, -1}, {-1, -1}}; + for (int[] ao : candidateAnchors) { + int ax = baseX + ao[0]; + int az = baseZ + ao[1]; + boolean all4Present = true; + for (int[] offset : FORMATION_2X2) { + if (!containsBlockLocation(choppedLogs, world, ax + offset[0], baseY, az + offset[1])) { + all4Present = false; + break; + } + } + if (all4Present) { + return true; + } + } + + return false; + } + + /** + * Returns {@code true} if {@code locations} contains a block-coordinate match + * for the given world and integer coordinates. Uses integer comparison to avoid + * floating-point or yaw/pitch equality issues. + */ + private static boolean containsBlockLocation(Set locations, World world, int x, int y, int z) { + for (Location loc : locations) { + if (loc.getWorld() == world && loc.getBlockX() == x && loc.getBlockY() == y && loc.getBlockZ() == z) { + return true; + } + } + return false; } /** From d7992ee62db9c2ac3cce09293f351c8e574c9bf7 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 7 Mar 2026 11:43:03 +0800 Subject: [PATCH 08/55] fix folia support --- .../java/org/milkteamc/autotreechop/AutoTreeChop.java | 10 +++++++--- .../milkteamc/autotreechop/ModrinthUpdateChecker.java | 10 ++++------ .../autotreechop/events/BlockBreakListener.java | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 91945c2..b81c02c 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -1,5 +1,6 @@ package org.milkteamc.autotreechop; +import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.io.File; import java.util.HashSet; import java.util.Locale; @@ -158,8 +159,7 @@ public void onEnable() { config.getPassword()); saveTask = new PlayerDataSaveTask(this, SAVE_THRESHOLD); - saveTask.runTaskTimerAsynchronously(this, SAVE_INTERVAL, SAVE_INTERVAL); - + UniversalScheduler.getScheduler(this).runTaskTimerAsynchronously(saveTask, SAVE_INTERVAL, SAVE_INTERVAL); autoTreeChopAPI = new AutoTreeChopAPI(this); playerConfigs = new ConcurrentHashMap<>(); initializeHooks(); @@ -268,7 +268,11 @@ public void onDisable() { getLogger().info("Saving all player data before shutdown..."); if (saveTask != null) { - saveTask.cancel(); + try { + saveTask.cancel(); + } catch (IllegalStateException ignored) { + // Task was never scheduled or already cancelled (e.g. Folia shutdown) + } } for (Map.Entry entry : playerConfigs.entrySet()) { diff --git a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java index b0a1f9a..2e9a726 100644 --- a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java +++ b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java @@ -177,9 +177,8 @@ public ModrinthUpdateChecker startPeriodicCheck() { checkNow(); long intervalTicks = checkIntervalHours * 60 * 60 * 20L; // hours to ticks - plugin.getServer() - .getScheduler() - .runTaskTimerAsynchronously(plugin, this::performCheck, intervalTicks, intervalTicks); + UniversalScheduler.getScheduler(plugin) + .runTaskTimerAsynchronously(this::performCheck, intervalTicks, intervalTicks); return this; } @@ -322,9 +321,8 @@ public void onPlayerJoin(PlayerJoinEvent event) { } if (shouldNotify) { - plugin.getServer() - .getScheduler() - .runTaskLater(plugin, () -> printCheckResultToPlayer(player, false), 40L); // 2s + UniversalScheduler.getScheduler(plugin) + .runTaskLater(() -> printCheckResultToPlayer(player, false), 40L); // 2s } } diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 6a7b181..d3fc50f 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -132,7 +132,7 @@ public void onBlockBreak(BlockBreakEvent event) { Location frozenLocation = location.clone(); ItemStack frozenTool = tool.clone(); - plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + scheduler.runTaskAsync(() -> { boolean hasLeaves = hasNearbyLeaves(block, config, snapshots); // Return to the main/region thread to act on the result. From f1e5bf00f55eb5460660230fbc180a4369e872b6 Mon Sep 17 00:00:00 2001 From: unclenet Date: Fri, 13 Mar 2026 09:31:56 +0000 Subject: [PATCH 09/55] [ci skip] Translated using Weblate (Italian) Currently translated at 100.0% (27 of 27 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/it/ --- src/main/resources/lang/it.properties | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/resources/lang/it.properties b/src/main/resources/lang/it.properties index 8b13789..e537c7f 100644 --- a/src/main/resources/lang/it.properties +++ b/src/main/resources/lang/it.properties @@ -1 +1,27 @@ - +noResidencePermissions=Non hai i permessi per usare AutoTreeChop qui. +enabled=AutoTreeChop attivato. +disabled=AutoTreeChop disattivato. +enabledByOther=AutoTreeChop è stato attivato da {player}. +enabledForOther=AutoTreeChop è stato attivato per {player}. +disabledByOther=AutoTreeChop è stato disattivato da {player}. +disabledForOther=AutoTreeChop è stato disattivato per {player}. +no-permission=Non hai i permessi per effettuare questa azione. +only-players=Questo comando può essere usato soltanto dai giocatori. +hitmaxusage=Hai raggiunto il tuo limite di utilizzo giornaliero. +hitmaxblock=Hai raggiunto il tuo limite giornaliero di blocchi distrutti. +usage=Utilizzi giornalieri di AutoTreeChop: {current_uses}/{max_uses} +blocks-broken=Blocchi distrutti oggi: {current_blocks}/{max_blocks} +stillInCooldown=AutoTreeChop è in cooldown. Per favore attendi {cooldown_time} secondi. +confirmationRequiredIdle=AutoTreeChop non è stato utilizzato di recente. Distruggi un tronco (o digita /atc confirm) entro {timeout} s per confermare. +confirmationRequiredNoLeaves=Non ci sono foglie rilevate nelle vicinanze. Questo tronco potrebbe essere stato piazzato da un giocatore. Taglia ancora (o digita /atc confirm) entro {timeout} s per confermare. +confirmationRequiredBoth=AutoTreeChop non è stato utilizzato di recente e non ci sono foglie rilevate nelle vicinanze. Taglia ancora (o digita /atc confirm) entro {timeout} s per confermare. +confirmationSuccess=Conferma effettuata. AutoTreeChop è ora attivo. +noPendingConfirmation=Non ci sono richieste di conferma di AutoTreeChop in sospeso. +sneakEnabled=AutoTreeChop è stato attivato durante l'accovacciamento. +sneakDisabled=AutoTreeChop disattivato dopo aver interrotto l'accovacciamento. +consoleName=console +aboutHeader=AutoTreeChop - v{version}di MilkTeaMC team e collaboratori +aboutLicense=Licenza: GNU General Public License v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutDiscord=Discord: https://discord.gg/uQ4UXANnP2 +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop From c7c0ce65ee7c32cfbdd9b7b3b9a99e1987a00491 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 16 Mar 2026 21:07:55 +0800 Subject: [PATCH 10/55] add null check --- .../milkteamc/autotreechop/AutoTreeChop.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index b81c02c..94f92de 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -275,29 +275,34 @@ public void onDisable() { } } - for (Map.Entry entry : playerConfigs.entrySet()) { - confirmationManager.clearPlayer(entry.getKey()); - if (entry.getValue().isDirty()) { - databaseManager.savePlayerDataSync(entry.getValue().getData()); + if (confirmationManager != null && playerConfigs != null) { + for (Map.Entry entry : playerConfigs.entrySet()) { + confirmationManager.clearPlayer(entry.getKey()); + if (entry.getValue().isDirty()) { + databaseManager.savePlayerDataSync(entry.getValue().getData()); + } } + playerConfigs.clear(); } - playerConfigs.clear(); - if (databaseManager != null) { databaseManager.close(); } - SessionManager sessionManager = SessionManager.getInstance(); - for (UUID uuid : new HashSet<>(playerConfigs.keySet())) { - sessionManager.clearAllPlayerSessions(uuid); + if (playerConfigs != null) { + SessionManager sessionManager = SessionManager.getInstance(); + for (UUID uuid : new HashSet<>(playerConfigs.keySet())) { + sessionManager.clearAllPlayerSessions(uuid); + } } if (translationManager != null) { translationManager.close(); } - metrics.shutdown(); + if (metrics != null) { + metrics.shutdown(); + } getLogger().info("AutoTreeChop disabled!"); } From bc38db859922b2eb898d86338aa3d6481a6d796e Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 16 Mar 2026 21:13:22 +0800 Subject: [PATCH 11/55] initial workaround for "special characters are not allowed" error --- .../org/milkteamc/autotreechop/Config.java | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/Config.java b/src/main/java/org/milkteamc/autotreechop/Config.java index 18987da..63333ba 100644 --- a/src/main/java/org/milkteamc/autotreechop/Config.java +++ b/src/main/java/org/milkteamc/autotreechop/Config.java @@ -9,10 +9,12 @@ import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -95,6 +97,11 @@ public Config(AutoTreeChop plugin) { public void load() { File configFile = new File(plugin.getDataFolder(), "config.yml"); + // Sanitize config file before parsing to remove illegal YAML characters + if (configFile.exists()) { + sanitizeConfigFile(configFile); + } + try { config = YamlDocument.create( configFile, @@ -140,6 +147,32 @@ public void load() { loadValues(); } + /** + * Removes illegal YAML characters from config.yml (e.g. UTF-8 BOM, null bytes, control characters). + */ + private void sanitizeConfigFile(File file) { + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + + // Remove UTF-8 BOM (EF BB BF) added by some Windows editors + if (bytes.length >= 3 + && (bytes[0] & 0xFF) == 0xEF + && (bytes[1] & 0xFF) == 0xBB + && (bytes[2] & 0xFF) == 0xBF) { + bytes = Arrays.copyOfRange(bytes, 3, bytes.length); + } + + // Remove null bytes and other control characters (keep \t \n \r) + String content = + new String(bytes, StandardCharsets.UTF_8).replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]", ""); + + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + + } catch (IOException e) { + plugin.getLogger().warning("Failed to sanitize config.yml: " + e.getMessage()); + } + } + private void loadValues() { visualEffect = config.getBoolean("visual-effect", true); toolDamage = config.getBoolean("toolDamage", true); @@ -496,23 +529,14 @@ public boolean isCallBlockBreakEvent() { return callBlockBreakEvent; } - /** - * Seconds of tree-chop inactivity before a confirmation is required. - */ public int getIdleTimeoutSeconds() { return idleTimeoutSeconds; } - /** - * Seconds the player has to re-chop a log (or run /atc confirm) after the warning. - */ public int getConfirmationWindowSeconds() { return confirmationWindowSeconds; } - /** - * Whether a confirmation is required when the target log has no nearby leaves. - */ public boolean isNoLeavesConfirmationEnabled() { return noLeavesConfirmationEnabled; } From 45cd9f0f978d06d3ae373bf91069d5b6fc028450 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 16 Mar 2026 21:13:37 +0800 Subject: [PATCH 12/55] bump to 1.7.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7869255..c53279c 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'org.milkteamc' -version = '1.7.3' +version = '1.7.4' // Java Configuration java { From 9ea5b41a2137bf6bd70bcd4e51df9559bbf0aace Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Fri, 20 Mar 2026 09:09:16 +0800 Subject: [PATCH 13/55] start moving to Matrix --- README.md | 4 ++-- src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java | 1 - .../java/org/milkteamc/autotreechop/command/AboutCommand.java | 1 - src/main/resources/lang/en.properties | 1 - src/main/resources/lang/es.properties | 1 - src/main/resources/lang/it.properties | 1 - src/main/resources/lang/zh.properties | 1 - 7 files changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 854cfbf..7d82930 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **AutoTreeChop** lets your players chop entire trees by breaking just one log. It's async-friendly, lightweight, and fully customizable — with built-in support for MySQL(optional), CoreProtect, and popular protection plugins. -- 🌐 [Discord Support Server](https://discord.gg/uQ4UXANnP2) +- 🌐 [Matrix Support Chat](https://matrix.to/#/#maoyue-dev:matrix.org) - 🌱 [Modrinth Page](https://modrinth.com/plugin/autotreechop) - 💻 [Source Code (GitHub)](https://github.com/milkteamc/AutoTreeChop) - ⚙️ [Default Config](https://github.com/milkteamc/AutoTreeChop/blob/master/src/main/resources/config.yml) @@ -100,7 +100,7 @@ It's async-friendly, lightweight, and fully customizable — with built-in suppo ## Support & Contribute -- Need help? Join our [Discord](https://discord.gg/uQ4UXANnP2) +- Need help? Join our [Matrix](https://matrix.to/#/#maoyue-dev:matrix.org) - Found a bug? Open an issue on [GitHub](https://github.com/milkteamc/AutoTreeChop/issues) - Want to help translate? Get started [here](https://translate.codeberg.org/projects/autotreechop/autotreechop) - Love the plugin? Give it a ⭐️ on [GitHub](https://github.com/milkteamc/AutoTreeChop) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 94f92de..88811b7 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -62,7 +62,6 @@ public class AutoTreeChop extends JavaPlugin { public static final String ABOUT_HEADER = "aboutHeader"; public static final String ABOUT_LICENSE = "aboutLicense"; public static final String ABOUT_GITHUB = "aboutGithub"; - public static final String ABOUT_DISCORD = "aboutDiscord"; public static final String ABOUT_MODRINTH = "aboutModrinth"; private static final long SAVE_INTERVAL = 1200L; // 60s diff --git a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java index 925e557..689a859 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java @@ -27,7 +27,6 @@ public void about(BukkitCommandActor actor) { AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_LICENSE); AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_GITHUB); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_DISCORD); AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_MODRINTH); } } diff --git a/src/main/resources/lang/en.properties b/src/main/resources/lang/en.properties index ee12fa6..af3d21e 100644 --- a/src/main/resources/lang/en.properties +++ b/src/main/resources/lang/en.properties @@ -35,5 +35,4 @@ consoleName=console aboutHeader=AutoTreeChop - v{version} by the MilkTeaMC team and contributors aboutLicense=License: GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub: https://github.com/milkteamc/autotreechop -aboutDiscord=Discord: https://discord.gg/uQ4UXANnP2 aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop diff --git a/src/main/resources/lang/es.properties b/src/main/resources/lang/es.properties index 9dfa37e..665ca73 100644 --- a/src/main/resources/lang/es.properties +++ b/src/main/resources/lang/es.properties @@ -23,5 +23,4 @@ sneakDisabled=La opción de hacer clic automáticamente se deshabilitó aboutHeader=AutoTreeChop - v{versión} por el equipo de MilkTeaMC y los colaboradores aboutLicense=Licencia: GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub: https://github.com/milkteamc/autotreechop -aboutDiscord=Discord: https://discord.gg/uQ4UXANnP2 aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop diff --git a/src/main/resources/lang/it.properties b/src/main/resources/lang/it.properties index e537c7f..e2fb2d1 100644 --- a/src/main/resources/lang/it.properties +++ b/src/main/resources/lang/it.properties @@ -23,5 +23,4 @@ consoleName=console aboutHeader=AutoTreeChop - v{version}di MilkTeaMC team e collaboratori aboutLicense=Licenza: GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub: https://github.com/milkteamc/autotreechop -aboutDiscord=Discord: https://discord.gg/uQ4UXANnP2 aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop diff --git a/src/main/resources/lang/zh.properties b/src/main/resources/lang/zh.properties index 9f4854e..c0708eb 100644 --- a/src/main/resources/lang/zh.properties +++ b/src/main/resources/lang/zh.properties @@ -35,5 +35,4 @@ consoleName=控制台 aboutHeader=AutoTreeChop - v{version} 由 MilkTeaMC 團隊與貢獻者開發 aboutLicense=授權條款:GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub:https://github.com/milkteamc/autotreechop -aboutDiscord=Discord:https://discord.gg/uQ4UXANnP2 aboutModrinth=Modrinth:https://modrinth.com/plugin/autotreechop From 2c2a784d299a41ff27c68d1972a3ed8a765473f5 Mon Sep 17 00:00:00 2001 From: ramses5 Date: Mon, 23 Mar 2026 01:41:25 +0000 Subject: [PATCH 14/55] [ci skip] Translated using Weblate (French) Currently translated at 100.0% (26 of 26 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/fr/ --- src/main/resources/lang/fr.properties | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/main/resources/lang/fr.properties b/src/main/resources/lang/fr.properties index 89d697f..560d27f 100644 --- a/src/main/resources/lang/fr.properties +++ b/src/main/resources/lang/fr.properties @@ -1,14 +1,26 @@ -blocks-broken=Tu as abattu {current_blocks}/{max_blocks} blocs aujourd'hui. -consoleName=Console -disabled=Abattage automatique désactivé. -disabledByOther=Abattage automatique désactivé par {player}. -disabledForOther=Abattage automatique désactivé pour {player} -enabled=Abattage automatique activé. -enabledByOther=Abattage automatique activé par {player} -enabledForOther=Abattage automatique activé pour {player} -hitmaxblock=Tu as atteint ta limite quotidienne de blocs pour l'abattage automatique. -hitmaxusage=Tu as atteint ta limite d'utilisation quotidienne pour l'abattage automatique. -no-permission=Tu n'as pas la permission de faire ça. -usage=Tu as utilisé l'abattage automatique {current_uses}/{max_uses} fois aujourd'hui. -noResidencePermissions=Tu n'as pas la permission d'utiliser l'Abattage Automatique ici. -stillInCooldown=Tu es encore en temps de recharge ! Réessaie après {cooldown_time} secondes. \ No newline at end of file +blocks-broken=Tu as cassé {current_blocks}/{max_blocks} blocs aujourd'hui. +consoleName=console +disabled=AutoTreeChop désactivé. +disabledByOther=AutoTreeChop désactivé par {player}. +disabledForOther=AutoTreeChop désactivé pour {player} +enabled=AutoTreeChop activé. +enabledByOther=AutoTreeChop activé par {player} +enabledForOther=AutoTreeChop activé pour {player} +hitmaxblock=Tu as atteint ta limite quotidienne de destruction de blocs. +hitmaxusage=Tu as atteint ta limite d'utilisation quotidienne. +no-permission=Tu n'as pas la permission de faire ça. +usage=Tu as utilisé AutoTreeChop {current_uses}/{max_uses} fois aujourd'hui. +noResidencePermissions=Tu n'as pas la permission d'utiliser AutoTreeChop ici. +stillInCooldown=Tu es encore en temps de recharge ! Réessaie après {cooldown_time} secondes. +only-players=Cette commande peut seulement être utilisée par les joueurs. +confirmationRequiredIdle=AutoTreeChop n'a pas été récemment utilisé. Coupe une nouvelle bûche (ou /atc confirm) avant {timeout}s pour confirmer. +confirmationSuccess=Confirmation réussie. AutoTreeChop est maintenant actif. +noPendingConfirmation=Il n'y a pas de confirmation d'AutoTreeChop en attente. +sneakEnabled=AutoTreeChop activé en étant accroupi. +sneakDisabled=AutoTreeChop désactivé en n'étant plus accroupi . +aboutHeader=AutoTreeChop - v{version} par l'équipe MilkTeaMC et les contributeurs +aboutLicense=Licence: GNU General Public License v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop +confirmationRequiredNoLeaves=Pas de feuillage détecté à proximité. Cette bûche a pu être placée par un joueur. Couper une nouvelle bûche (ou /atc confirm) avant {timeout}s pour confirmer. +confirmationRequiredBoth=AutoTreeChop n'a pas été récemment utilisé et aucun feuillage n'a été détecté à proximité. Coupez une nouvelle bûche (ou /atc confirm) avant {timeout}s pour confirmer. From 24d732988ef50dceacf7b77454d79dc2b7a7251c Mon Sep 17 00:00:00 2001 From: ramses5 Date: Mon, 23 Mar 2026 01:42:33 +0000 Subject: [PATCH 15/55] [ci skip] Translated using Weblate (Italian) Currently translated at 100.0% (26 of 26 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/it/ --- src/main/resources/lang/it.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/lang/it.properties b/src/main/resources/lang/it.properties index e2fb2d1..8dfdd28 100644 --- a/src/main/resources/lang/it.properties +++ b/src/main/resources/lang/it.properties @@ -20,7 +20,7 @@ noPendingConfirmation=Non ci sono richieste di conferma di AutoTreeChop in sneakEnabled=AutoTreeChop è stato attivato durante l'accovacciamento. sneakDisabled=AutoTreeChop disattivato dopo aver interrotto l'accovacciamento. consoleName=console -aboutHeader=AutoTreeChop - v{version}di MilkTeaMC team e collaboratori +aboutHeader=AutoTreeChop - v{version} di MilkTeaMC team e collaboratori aboutLicense=Licenza: GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub: https://github.com/milkteamc/autotreechop aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop From 8e8cb6d682ef1a0156cc5ca66c21415e9f928b3b Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Wed, 25 Mar 2026 17:12:15 +0800 Subject: [PATCH 16/55] ensure player without permission cant chop tree --- .../java/org/milkteamc/autotreechop/utils/TreeChopUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 1494a5c..9d8b60f 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -136,6 +136,11 @@ public void chopTree( PlayerConfig playerConfig, ProtectionCheckUtils.ProtectionHooks hooks) { + if (!player.hasPermission("autotreechop.use")) { + playerConfig.setAutoTreeChopEnabled(false); + return; + } + // Initial protection check if (!ProtectionCheckUtils.canModifyBlock(player, location, hooks)) { return; From 8a1ccf951526a6ddd289e5eece054dc85b516fed Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Wed, 25 Mar 2026 17:22:21 +0800 Subject: [PATCH 17/55] more checking --- .../java/org/milkteamc/autotreechop/utils/TreeChopUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 9d8b60f..06f5cc8 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -212,6 +212,11 @@ private void validateAndExecuteChop( UUID playerUUID = player.getUniqueId(); + if (!player.hasPermission("autotreechop.use")) { + sessionManager.clearTreeChopSession(playerUUID); + return; + } + // Validation checks if (treeBlocks.isEmpty()) { sessionManager.clearTreeChopSession(playerUUID); From d7945d33a23b066b89a715376b3d0969f1e689fd Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 28 Mar 2026 12:51:57 +0800 Subject: [PATCH 18/55] [ci skip] add license header --- README.md | 20 +++++++++++++++++++ build.gradle | 3 ++- spotless/HEADER | 17 ++++++++++++++++ .../milkteamc/autotreechop/AutoTreeChop.java | 17 ++++++++++++++++ .../autotreechop/AutoTreeChopAPI.java | 17 ++++++++++++++++ .../autotreechop/AutoTreeChopExpansion.java | 17 ++++++++++++++++ .../org/milkteamc/autotreechop/Config.java | 17 ++++++++++++++++ .../autotreechop/ModrinthUpdateChecker.java | 17 ++++++++++++++++ .../milkteamc/autotreechop/PlayerConfig.java | 17 ++++++++++++++++ .../autotreechop/command/AboutCommand.java | 17 ++++++++++++++++ .../autotreechop/command/ConfirmCommand.java | 17 ++++++++++++++++ .../autotreechop/command/ReloadCommand.java | 17 ++++++++++++++++ .../autotreechop/command/ToggleCommand.java | 17 ++++++++++++++++ .../autotreechop/command/UsageCommand.java | 17 ++++++++++++++++ .../database/DatabaseManager.java | 17 ++++++++++++++++ .../events/BlockBreakListener.java | 17 ++++++++++++++++ .../events/PlayerJoinListener.java | 17 ++++++++++++++++ .../events/PlayerQuitListener.java | 17 ++++++++++++++++ .../events/PlayerSneakListener.java | 17 ++++++++++++++++ .../hooks/GriefPreventionHook.java | 17 ++++++++++++++++ .../autotreechop/hooks/LandsHook.java | 17 ++++++++++++++++ .../autotreechop/hooks/ResidenceHook.java | 17 ++++++++++++++++ .../autotreechop/hooks/WorldGuardHook.java | 17 ++++++++++++++++ .../tasks/PlayerDataSaveTask.java | 17 ++++++++++++++++ .../translation/MessageFormatter.java | 17 ++++++++++++++++ .../translation/StyleRegistry.java | 17 ++++++++++++++++ .../translation/TranslationManager.java | 17 ++++++++++++++++ .../utils/AsyncTaskScheduler.java | 17 ++++++++++++++++ .../autotreechop/utils/BatchProcessor.java | 17 ++++++++++++++++ .../utils/BlockDiscoveryUtils.java | 17 ++++++++++++++++ .../autotreechop/utils/BlockSnapshot.java | 17 ++++++++++++++++ .../utils/BlockSnapshotCreator.java | 17 ++++++++++++++++ .../utils/ConfirmationManager.java | 17 ++++++++++++++++ .../autotreechop/utils/CooldownManager.java | 17 ++++++++++++++++ .../autotreechop/utils/EffectUtils.java | 17 ++++++++++++++++ .../autotreechop/utils/PermissionUtils.java | 17 ++++++++++++++++ .../utils/ProtectionCheckUtils.java | 17 ++++++++++++++++ .../autotreechop/utils/SessionManager.java | 17 ++++++++++++++++ .../autotreechop/utils/TreeChopUtils.java | 17 ++++++++++++++++ .../autotreechop/utils/TreeReplantUtils.java | 17 ++++++++++++++++ 40 files changed, 668 insertions(+), 1 deletion(-) create mode 100644 spotless/HEADER diff --git a/README.md b/README.md index 7d82930..1cb1243 100644 --- a/README.md +++ b/README.md @@ -114,3 +114,23 @@ If you prefer not to participate, you can disable it anytime in: `/plugins/bStats/config.yml` [![bstats](https://bstats.org/signatures/bukkit/AutoTreeChop.svg)](https://bstats.org/plugin/bukkit/AutoTreeChop/20053) + +--- + +## License +``` + 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 . +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index c53279c..793dee4 100644 --- a/build.gradle +++ b/build.gradle @@ -29,8 +29,9 @@ spotless { java { removeUnusedImports() formatAnnotations() - palantirJavaFormat() + + licenseHeaderFile(rootProject.file('spotless/HEADER')) } } diff --git a/spotless/HEADER b/spotless/HEADER new file mode 100644 index 0000000..371802a --- /dev/null +++ b/spotless/HEADER @@ -0,0 +1,17 @@ +/* + * Copyright (C) $YEAR 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 . + */ + \ No newline at end of file diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 88811b7..3df1d6b 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -1,3 +1,20 @@ +/* + * 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; import com.github.Anon8281.universalScheduler.UniversalScheduler; diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java index db7f455..734a1be 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopAPI.java @@ -1,3 +1,20 @@ +/* + * 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; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java index e5046ac..05511f1 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java @@ -1,3 +1,20 @@ +/* + * 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; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/Config.java b/src/main/java/org/milkteamc/autotreechop/Config.java index 63333ba..d39dfe5 100644 --- a/src/main/java/org/milkteamc/autotreechop/Config.java +++ b/src/main/java/org/milkteamc/autotreechop/Config.java @@ -1,3 +1,20 @@ +/* + * 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; import com.cryptomorin.xseries.XMaterial; diff --git a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java index 2e9a726..0e10e19 100644 --- a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java +++ b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java @@ -1,3 +1,20 @@ +/* + * 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; import com.github.Anon8281.universalScheduler.UniversalScheduler; diff --git a/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java b/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java index cc5e4ae..25808c3 100644 --- a/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java +++ b/src/main/java/org/milkteamc/autotreechop/PlayerConfig.java @@ -1,3 +1,20 @@ +/* + * 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; import java.time.LocalDate; diff --git a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java index 689a859..3c3bb82 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java @@ -1,3 +1,20 @@ +/* + * 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.command; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; diff --git a/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java index 08def6a..bdd614d 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java @@ -1,3 +1,20 @@ +/* + * 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.command; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/command/ReloadCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ReloadCommand.java index 30931e7..ed94039 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ReloadCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ReloadCommand.java @@ -1,3 +1,20 @@ +/* + * 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.command; import org.milkteamc.autotreechop.AutoTreeChop; diff --git a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java index a8fa059..8cc4d5f 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java @@ -1,3 +1,20 @@ +/* + * 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.command; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java index 3f57b18..184b971 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java @@ -1,3 +1,20 @@ +/* + * 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.command; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; diff --git a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java index bcdb242..9b71380 100644 --- a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java +++ b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java @@ -1,3 +1,20 @@ +/* + * 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.zaxxer.hikari.HikariConfig; diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index d3fc50f..7405fba 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -1,3 +1,20 @@ +/* + * 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.events; import java.util.HashMap; diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java index 98d781e..8ccb67c 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java @@ -1,3 +1,20 @@ +/* + * 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.events; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java index ba342df..4fdcfe1 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerQuitListener.java @@ -1,3 +1,20 @@ +/* + * 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.events; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java index cd7e571..5c895a2 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java @@ -1,3 +1,20 @@ +/* + * 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.events; import java.util.UUID; diff --git a/src/main/java/org/milkteamc/autotreechop/hooks/GriefPreventionHook.java b/src/main/java/org/milkteamc/autotreechop/hooks/GriefPreventionHook.java index 12cd151..3f22c2a 100644 --- a/src/main/java/org/milkteamc/autotreechop/hooks/GriefPreventionHook.java +++ b/src/main/java/org/milkteamc/autotreechop/hooks/GriefPreventionHook.java @@ -1,3 +1,20 @@ +/* + * 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 me.ryanhamshire.GriefPrevention.Claim; diff --git a/src/main/java/org/milkteamc/autotreechop/hooks/LandsHook.java b/src/main/java/org/milkteamc/autotreechop/hooks/LandsHook.java index e92797d..58457cf 100644 --- a/src/main/java/org/milkteamc/autotreechop/hooks/LandsHook.java +++ b/src/main/java/org/milkteamc/autotreechop/hooks/LandsHook.java @@ -1,3 +1,20 @@ +/* + * 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 me.angeschossen.lands.api.LandsIntegration; diff --git a/src/main/java/org/milkteamc/autotreechop/hooks/ResidenceHook.java b/src/main/java/org/milkteamc/autotreechop/hooks/ResidenceHook.java index 5d0a64b..f1a3452 100644 --- a/src/main/java/org/milkteamc/autotreechop/hooks/ResidenceHook.java +++ b/src/main/java/org/milkteamc/autotreechop/hooks/ResidenceHook.java @@ -1,3 +1,20 @@ +/* + * 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 com.bekvon.bukkit.residence.api.ResidenceApi; diff --git a/src/main/java/org/milkteamc/autotreechop/hooks/WorldGuardHook.java b/src/main/java/org/milkteamc/autotreechop/hooks/WorldGuardHook.java index 58463ca..b11b98d 100644 --- a/src/main/java/org/milkteamc/autotreechop/hooks/WorldGuardHook.java +++ b/src/main/java/org/milkteamc/autotreechop/hooks/WorldGuardHook.java @@ -1,3 +1,20 @@ +/* + * 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 com.sk89q.worldedit.bukkit.BukkitAdapter; diff --git a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java index 13b9aa3..40957a4 100644 --- a/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java +++ b/src/main/java/org/milkteamc/autotreechop/tasks/PlayerDataSaveTask.java @@ -1,3 +1,20 @@ +/* + * 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.tasks; import com.github.Anon8281.universalScheduler.UniversalRunnable; diff --git a/src/main/java/org/milkteamc/autotreechop/translation/MessageFormatter.java b/src/main/java/org/milkteamc/autotreechop/translation/MessageFormatter.java index 9410e50..11aa9b0 100644 --- a/src/main/java/org/milkteamc/autotreechop/translation/MessageFormatter.java +++ b/src/main/java/org/milkteamc/autotreechop/translation/MessageFormatter.java @@ -1,3 +1,20 @@ +/* + * 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.translation; import java.util.Map; diff --git a/src/main/java/org/milkteamc/autotreechop/translation/StyleRegistry.java b/src/main/java/org/milkteamc/autotreechop/translation/StyleRegistry.java index 696435e..26084b9 100644 --- a/src/main/java/org/milkteamc/autotreechop/translation/StyleRegistry.java +++ b/src/main/java/org/milkteamc/autotreechop/translation/StyleRegistry.java @@ -1,3 +1,20 @@ +/* + * 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.translation; import java.io.File; diff --git a/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java b/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java index 6d601b5..5eb2a96 100644 --- a/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java +++ b/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java @@ -1,3 +1,20 @@ +/* + * 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.translation; import java.io.*; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/AsyncTaskScheduler.java b/src/main/java/org/milkteamc/autotreechop/utils/AsyncTaskScheduler.java index 1a77cd6..425b892 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/AsyncTaskScheduler.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/AsyncTaskScheduler.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import com.github.Anon8281.universalScheduler.UniversalScheduler; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/BatchProcessor.java b/src/main/java/org/milkteamc/autotreechop/utils/BatchProcessor.java index 92860d9..baea53d 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/BatchProcessor.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/BatchProcessor.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import java.util.List; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/BlockDiscoveryUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/BlockDiscoveryUtils.java index e1e274c..0011f07 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/BlockDiscoveryUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/BlockDiscoveryUtils.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import java.util.HashSet; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshot.java b/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshot.java index ba53df6..ac8bb78 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshot.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshot.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import com.cryptomorin.xseries.XMaterial; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshotCreator.java b/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshotCreator.java index d7b55be..182c319 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshotCreator.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/BlockSnapshotCreator.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import java.util.HashMap; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/ConfirmationManager.java b/src/main/java/org/milkteamc/autotreechop/utils/ConfirmationManager.java index e5eb8a5..7c1bd61 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/ConfirmationManager.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/ConfirmationManager.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import java.util.Map; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/CooldownManager.java b/src/main/java/org/milkteamc/autotreechop/utils/CooldownManager.java index 0ec1cb9..89257c8 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/CooldownManager.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/CooldownManager.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import java.util.HashMap; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java index eb0a26d..3151a95 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import static org.milkteamc.autotreechop.AutoTreeChop.HIT_MAX_BLOCK_MESSAGE; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/PermissionUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/PermissionUtils.java index eb3a87c..edd8c09 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/PermissionUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/PermissionUtils.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import org.bukkit.entity.Player; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/ProtectionCheckUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/ProtectionCheckUtils.java index 7768fca..e9ac540 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/ProtectionCheckUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/ProtectionCheckUtils.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import org.bukkit.Location; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java b/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java index e25cf47..e3f758c 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/SessionManager.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import java.util.ArrayList; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 06f5cc8..880b618 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import com.cryptomorin.xseries.XMaterial; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java index 57cb190..17c3f03 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java @@ -1,3 +1,20 @@ +/* + * 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.utils; import com.cryptomorin.xseries.XMaterial; From 0cd2900cb25a5193478a6c5301e8ee70eed52612 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 28 Mar 2026 13:37:50 +0800 Subject: [PATCH 19/55] [ci skip] yes, use matrix --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 793dee4..d303838 100644 --- a/build.gradle +++ b/build.gradle @@ -196,7 +196,7 @@ modrinth { --- 📝 Report issues: [GitHub Issues](https://github.com/milkteamc/AutoTreeChop/issues) - 💬 Get support: [Discord](https://discord.gg/uQ4UXANnP2) + 💬 Get support: [Matrix](https://matrix.to/#/#maoyue-dev:matrix.org) """.stripIndent().trim() uploadFile = tasks.shadowJar.archiveFile From a0bc741c6ff569334c4cff3606e248b152871a00 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 28 Mar 2026 17:37:21 +0800 Subject: [PATCH 20/55] update actions version --- .github/workflows/Java-CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Java-CI.yml b/.github/workflows/Java-CI.yml index 99f8f1d..c056bdd 100644 --- a/.github/workflows/Java-CI.yml +++ b/.github/workflows/Java-CI.yml @@ -14,8 +14,8 @@ jobs: runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 with: distribution: "temurin" java-version: "17" From 8fa0fa60ae33678c1c33a1f61640d495a56db78b Mon Sep 17 00:00:00 2001 From: MoNova786 Date: Tue, 31 Mar 2026 13:22:11 +0000 Subject: [PATCH 21/55] [ci skip] Translated using Weblate (German) Currently translated at 61.5% (16 of 26 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/de/ --- src/main/resources/lang/de.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/lang/de.properties b/src/main/resources/lang/de.properties index 4ae770e..c48b169 100644 --- a/src/main/resources/lang/de.properties +++ b/src/main/resources/lang/de.properties @@ -10,3 +10,7 @@ hitmaxblock=Du hast dein tägliches Blocklimit zum Auto-Baumfä hitmaxusage=Du hast dein Tageslimit zum Auto-Baumfällen erreicht. no-permission=Dazu hast du keine Berechtigung usage=Du hast Auto-Baumfällen heute {current_uses}/{max_uses} mal verwendet. +noResidencePermissions=Du hast keine Berechtigung AutoTreeChop hier zu nutzen. +only-players=Dieser Befehl kann nur von Spielern benutzt werden. +stillInCooldown=AutoTreeChop kühlt sich ab. Bitte warte {cooldown_time} seconds. +confirmationRequiredIdle=AutoTreeChop wurde in der letzten Zeit nicht benutzt. Fälle ein Stamm erneut (oder/atc confirm) inerhalb {timeout}s to confirm. From c43cff246c6e2e748cbcef650379d45bfe3cd35c Mon Sep 17 00:00:00 2001 From: Klimperfix Date: Thu, 2 Apr 2026 13:59:13 +0000 Subject: [PATCH 22/55] [ci skip] Translated using Weblate (German) Currently translated at 100.0% (26 of 26 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/de/ --- src/main/resources/lang/de.properties | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/resources/lang/de.properties b/src/main/resources/lang/de.properties index c48b169..b41cbad 100644 --- a/src/main/resources/lang/de.properties +++ b/src/main/resources/lang/de.properties @@ -14,3 +14,13 @@ noResidencePermissions=Du hast keine Berechtigung AutoTreeChop hier zu nutz only-players=Dieser Befehl kann nur von Spielern benutzt werden. stillInCooldown=AutoTreeChop kühlt sich ab. Bitte warte {cooldown_time} seconds. confirmationRequiredIdle=AutoTreeChop wurde in der letzten Zeit nicht benutzt. Fälle ein Stamm erneut (oder/atc confirm) inerhalb {timeout}s to confirm. +confirmationRequiredNoLeaves=Keine Blätter in der Nähe erkannt. Dieser Baumstamm könnte von Spielern plaziert sein. Schlag wieder (oder /atc bestätige) innerhalb von {timeout}s um zu bestätigen. +confirmationRequiredBoth=AutoTreeChop wurde längere Zeit nicht verwendet und in der Nähe wurden keine Blätter erkannt. Schlag nochmal (oder /atc bestätige) innerhalb von {timeout}s um zu bestätigen. +confirmationSuccess=Erfolgreich bestätigt. AutoTreeChop ist nun aktiv. +noPendingConfirmation=Es gibt keine vorhandende AutoTreeChop-Bestätigung. +sneakEnabled=AutoTreeChop wurde beim Schleichen aktiviert. +sneakDisabled=AutoTreeChop wurde nach Ende des Schleichens deaktiviert. +aboutHeader=AutoTreeChop - v{version} vom MilkTeaMC-Team und Contributors +aboutLicense=License: GNU General Public License v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop From 9afbd0b27b5a323e89d016caa4a565c2d87953c7 Mon Sep 17 00:00:00 2001 From: metrokitten Date: Fri, 3 Apr 2026 19:17:34 +0000 Subject: [PATCH 23/55] [ci skip] Added translation using Weblate (Turkish) --- src/main/resources/lang/tr.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/lang/tr.properties diff --git a/src/main/resources/lang/tr.properties b/src/main/resources/lang/tr.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/lang/tr.properties @@ -0,0 +1 @@ + From 66566f5fe029686467b5e79d6b3863f0bcc840e1 Mon Sep 17 00:00:00 2001 From: Metr Date: Fri, 3 Apr 2026 23:51:36 +0300 Subject: [PATCH 24/55] [fix] remove deprecated method of getting plugin metadata --- .../java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java index 0e10e19..1f7a5de 100644 --- a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java +++ b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java @@ -90,7 +90,7 @@ public enum UpdateCheckResult { public ModrinthUpdateChecker(@NotNull JavaPlugin plugin, @NotNull String projectId, @NotNull String loader) { this.plugin = plugin; this.projectId = projectId; - this.currentVersion = plugin.getDescription().getVersion(); + this.currentVersion = plugin.getPluginMeta().getVersion(); this.loader = loader; this.minecraftVersion = plugin.getServer().getBukkitVersion().split("-")[0]; } From 2d73f80686a08c12ff3ce4d24fa0eecd775a0599 Mon Sep 17 00:00:00 2001 From: metrokitten Date: Fri, 3 Apr 2026 19:45:04 +0000 Subject: [PATCH 25/55] [ci skip] Translated using Weblate (German) Currently translated at 100.0% (26 of 26 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/de/ --- src/main/resources/lang/de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/lang/de.properties b/src/main/resources/lang/de.properties index b41cbad..898888c 100644 --- a/src/main/resources/lang/de.properties +++ b/src/main/resources/lang/de.properties @@ -21,6 +21,6 @@ noPendingConfirmation=Es gibt keine vorhandende AutoTreeChop-Bestätigung.< sneakEnabled=AutoTreeChop wurde beim Schleichen aktiviert. sneakDisabled=AutoTreeChop wurde nach Ende des Schleichens deaktiviert. aboutHeader=AutoTreeChop - v{version} vom MilkTeaMC-Team und Contributors -aboutLicense=License: GNU General Public License v3.0 (GPL-3.0) +aboutLicense=Lizenz: GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub: https://github.com/milkteamc/autotreechop aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop From 9f1a7472d17f68af6c5d571cd40f5af5897545aa Mon Sep 17 00:00:00 2001 From: metrokitten Date: Fri, 3 Apr 2026 19:42:43 +0000 Subject: [PATCH 26/55] [ci skip] Translated using Weblate (Turkish) Currently translated at 100.0% (26 of 26 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/tr/ --- src/main/resources/lang/tr.properties | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/resources/lang/tr.properties b/src/main/resources/lang/tr.properties index 8b13789..d1d0f47 100644 --- a/src/main/resources/lang/tr.properties +++ b/src/main/resources/lang/tr.properties @@ -1 +1,26 @@ - +noResidencePermissions=Burada AutoTreeChopper'ı kullanmak için izniniz yok. +enabled=AutoTreeChop aktif. +disabled=AutoTreeChop devre dışı. +enabledByOther=AutoTreeChop {player} tarafından etkinleştirildi. +enabledForOther=AutoTreeChop {player} için etkinleştirildi. +disabledByOther=AutoTreeChop {player} tarafından devre dışı bırakıldı. +disabledForOther=AutoTreeChop {player} için devre dışı bırakıldı. +no-permission=Bu eylemi gerçekleştirmek için izniniz yok. +only-players=Bu komut sadece oyuncular tarafından kullanılabilir. +hitmaxusage=Günlük kullanım limitinize ulaştınız. +hitmaxblock=Günlük blok kırma limitinize ulaştınız. +usage=Günlük AutoTreeChop kullanımı: {current_uses}/{max_uses} +blocks-broken=Bugün kırılan bloklar: {current_blocks}/{max_blocks} +stillInCooldown=AutoTreeChop dinleniyor. Lütfen {cooldown_time} saniye bekleyiniz. +confirmationRequiredIdle=AutoTreeChop son zamanlarda kullanılmadı. Onaylamak için {timeout}s içinde tekrar bir kütük kesin (veya /atc confirm) komutunu çalıştırın. +confirmationRequiredNoLeaves=Yakınlarda yaprak tespit edilmedi. Bu kütük oyuncu tarafından yerleştirilmiş olabilir. Onaylamak için {timeout}s içinde tekrar kesin (veya /atc confirm) komutunu kullanın. +confirmationRequiredBoth=AutoTreeChop yakın zamanda kullanılmadı ve yakınlarda yaprak tespit edilmedi. Onaylamak için {timeout}s içinde tekrar kesin (veya /atc confirm komutunu çalıştırın). +confirmationSuccess=Onaylandı. AutoTreeChop şimdi aktif. +noPendingConfirmation=Beklenen bir AutoTreeChop onayı yok. +sneakEnabled=AutoTreeChop eğilirken aktif olacak. +sneakDisabled=AutoTreeChop eğildikten sonra devre dışı. +consoleName=konsol +aboutHeader=AutoTreeChop - v{version} MilkTeaMC ekibi ve katkıda bulunanlar tarafından +aboutLicense=Lisans: GNU Genel Kamu Lisansı v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutModrinth=Modrinth Sitesi: https://modrinth.com/plugin/autotreechop From a107f9aebb4e944a54c83aeab593cc8189ac5aae Mon Sep 17 00:00:00 2001 From: Metr Date: Fri, 3 Apr 2026 23:57:52 +0300 Subject: [PATCH 27/55] Properly lint the README --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1cb1243..64a0763 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # AutoTreeChop + ![atc-intro](https://github.com/user-attachments/assets/7b556970-7c4c-4271-9016-4a4612895379) **AutoTreeChop** lets your players chop entire trees by breaking just one log. It's async-friendly, lightweight, and fully customizable — with built-in support for MySQL(optional), CoreProtect, and popular protection plugins. @@ -13,38 +14,46 @@ It's async-friendly, lightweight, and fully customizable — with built-in suppo ## Key Features ### 🌲 Smart Tree Chopping + - Chop entire trees by breaking just one log - Toggle on/off with `/atc` command or by sneaking (pressing SHIFT) - Async support for smooth performance on Modern servers - Customizable leaves cleaner ### ⚡ Lightweight & Easy to Configure + - Minimal performance impact - Simple setup, and user-friendly configuration ### 🔁 Auto Replanting + - Automatically replant saplings after chopping - Optionally require players to have saplings ### 🧑‍🤝‍🧑 Player Control & Limits + - Daily limits for usage and chopped blocks - Configurable cooldowns - VIP players can bypass limits with permission ### 🛡️ Full Protection Plugin Support + - Compatible with Residence, WorldGuard, Lands, GriefPrevention - Supports **CoreProtect** for logging actions ### 🌐 Multi-Language & Locale Support + - Translations included: `en`, `zh`, `ja`, `de`, `es`, `fr`, `ru`, etc. - Automatically switches to player's locale if enabled ### 🗄️ MySQL & SQLite Support + - Scale with MySQL or keep it simple with SQLite (default) --- ## Supported Plugins +> > Since we call the block break event directly by default, plugins such as CoreProtect and Drop2Inventory should be supported without modification. - WorldGuard @@ -102,12 +111,13 @@ It's async-friendly, lightweight, and fully customizable — with built-in suppo - Need help? Join our [Matrix](https://matrix.to/#/#maoyue-dev:matrix.org) - Found a bug? Open an issue on [GitHub](https://github.com/milkteamc/AutoTreeChop/issues) -- Want to help translate? Get started [here](https://translate.codeberg.org/projects/autotreechop/autotreechop) +- Want to help translate? Get started [on this page](https://translate.codeberg.org/projects/autotreechop/autotreechop) - Love the plugin? Give it a ⭐️ on [GitHub](https://github.com/milkteamc/AutoTreeChop) --- ## bStats + This plugin uses [bStats](https://bstats.org) to collect anonymous usage statistics (such as plugin version, server software, and player count). These statistics help us improve the plugin. If you prefer not to participate, you can disable it anytime in: @@ -118,7 +128,8 @@ If you prefer not to participate, you can disable it anytime in: --- ## License -``` + +```txt Copyright (C) 2026 MilkTeaMC and contributors This program is free software: you can redistribute it and/or modify @@ -133,4 +144,4 @@ If you prefer not to participate, you can disable it anytime in: You should have received a copy of the GNU General Public License along with this program. If not, see . -``` \ No newline at end of file +``` From 4fd099e9649d6f2869792dc28fcee21a0c7d31fc Mon Sep 17 00:00:00 2001 From: Metr Date: Sat, 4 Apr 2026 00:00:03 +0300 Subject: [PATCH 28/55] add Turkish translation from Codeberg --- src/main/resources/lang/tr.properties | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main/resources/lang/tr.properties b/src/main/resources/lang/tr.properties index 8b13789..afd7727 100644 --- a/src/main/resources/lang/tr.properties +++ b/src/main/resources/lang/tr.properties @@ -1 +1,37 @@ +noResidencePermissions=Burada AutoTreeChopper'ı kullanmak için izniniz yok. +enabled=AutoTreeChop aktif. +disabled=AutoTreeChop devre dışı. + +enabledByOther=AutoTreeChop {player} tarafından etkinleştirildi. +enabledForOther=AutoTreeChop {player} için etkinleştirildi. + +disabledByOther=AutoTreeChop {player} tarafından devre dışı bırakıldı. +disabledForOther=AutoTreeChop {player} için devre dışı bırakıldı. + +no-permission=Bu eylemi gerçekleştirmek için izniniz yok. +only-players=Bu komut sadece oyuncular tarafından kullanılabilir. + +hitmaxusage=Günlük kullanım limitinize ulaştınız. +hitmaxblock=Günlük blok kırma limitinize ulaştınız. + +usage=Günlük AutoTreeChop kullanımı: {current_uses}/{max_uses} +blocks-broken=Bugün kırılan bloklar: {current_blocks}/{max_blocks} + +stillInCooldown=AutoTreeChop dinleniyor. Lütfen {cooldown_time} saniye bekleyiniz. + +confirmationRequiredIdle=AutoTreeChop son zamanlarda kullanılmadı. Onaylamak için {timeout}s içinde tekrar bir kütük kesin (veya /atc confirm) komutunu çalıştırın. +confirmationRequiredNoLeaves=Yakınlarda yaprak tespit edilmedi. Bu kütük oyuncu tarafından yerleştirilmiş olabilir. Onaylamak için {timeout}s içinde tekrar kesin (veya /atc confirm) komutunu kullanın. +confirmationRequiredBoth=AutoTreeChop yakın zamanda kullanılmadı ve yakınlarda yaprak tespit edilmedi. Onaylamak için {timeout}s içinde tekrar kesin (veya /atc confirm komutunu çalıştırın). + +confirmationSuccess=Onaylandı. AutoTreeChop şimdi aktif. +noPendingConfirmation=Beklenen bir AutoTreeChop onayı yok. + +sneakEnabled=AutoTreeChop eğilirken aktif olacak. +sneakDisabled=AutoTreeChop eğildikten sonra devre dışı. + +consoleName=konsol +aboutHeader=AutoTreeChop - v{version} MilkTeaMC ekibi ve katkıda bulunanlar tarafından +aboutLicense=Lisans: GNU Genel Kamu Lisansı v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutModrinth=Modrinth Sitesi: https://modrinth.com/plugin/autotreechop From a51ce4f7b65e80f9e01e47916c8fa88b7ae05891 Mon Sep 17 00:00:00 2001 From: Metr Date: Sat, 4 Apr 2026 00:07:59 +0300 Subject: [PATCH 29/55] Revert "add Turkish translation from Codeberg" This reverts commit 4fd099e9649d6f2869792dc28fcee21a0c7d31fc. --- src/main/resources/lang/tr.properties | 36 --------------------------- 1 file changed, 36 deletions(-) diff --git a/src/main/resources/lang/tr.properties b/src/main/resources/lang/tr.properties index afd7727..8b13789 100644 --- a/src/main/resources/lang/tr.properties +++ b/src/main/resources/lang/tr.properties @@ -1,37 +1 @@ -noResidencePermissions=Burada AutoTreeChopper'ı kullanmak için izniniz yok. -enabled=AutoTreeChop aktif. -disabled=AutoTreeChop devre dışı. - -enabledByOther=AutoTreeChop {player} tarafından etkinleştirildi. -enabledForOther=AutoTreeChop {player} için etkinleştirildi. - -disabledByOther=AutoTreeChop {player} tarafından devre dışı bırakıldı. -disabledForOther=AutoTreeChop {player} için devre dışı bırakıldı. - -no-permission=Bu eylemi gerçekleştirmek için izniniz yok. -only-players=Bu komut sadece oyuncular tarafından kullanılabilir. - -hitmaxusage=Günlük kullanım limitinize ulaştınız. -hitmaxblock=Günlük blok kırma limitinize ulaştınız. - -usage=Günlük AutoTreeChop kullanımı: {current_uses}/{max_uses} -blocks-broken=Bugün kırılan bloklar: {current_blocks}/{max_blocks} - -stillInCooldown=AutoTreeChop dinleniyor. Lütfen {cooldown_time} saniye bekleyiniz. - -confirmationRequiredIdle=AutoTreeChop son zamanlarda kullanılmadı. Onaylamak için {timeout}s içinde tekrar bir kütük kesin (veya /atc confirm) komutunu çalıştırın. -confirmationRequiredNoLeaves=Yakınlarda yaprak tespit edilmedi. Bu kütük oyuncu tarafından yerleştirilmiş olabilir. Onaylamak için {timeout}s içinde tekrar kesin (veya /atc confirm) komutunu kullanın. -confirmationRequiredBoth=AutoTreeChop yakın zamanda kullanılmadı ve yakınlarda yaprak tespit edilmedi. Onaylamak için {timeout}s içinde tekrar kesin (veya /atc confirm komutunu çalıştırın). - -confirmationSuccess=Onaylandı. AutoTreeChop şimdi aktif. -noPendingConfirmation=Beklenen bir AutoTreeChop onayı yok. - -sneakEnabled=AutoTreeChop eğilirken aktif olacak. -sneakDisabled=AutoTreeChop eğildikten sonra devre dışı. - -consoleName=konsol -aboutHeader=AutoTreeChop - v{version} MilkTeaMC ekibi ve katkıda bulunanlar tarafından -aboutLicense=Lisans: GNU Genel Kamu Lisansı v3.0 (GPL-3.0) -aboutGithub=GitHub: https://github.com/milkteamc/autotreechop -aboutModrinth=Modrinth Sitesi: https://modrinth.com/plugin/autotreechop From 72f725a361aae907c645d9bcecc336928c1e8ff4 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 4 Apr 2026 14:44:47 +0800 Subject: [PATCH 30/55] use 26.1.1 to test --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d303838..bafa601 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ version = '1.7.4' // Java Configuration java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) } } @@ -172,7 +172,7 @@ tasks.shadowJar { } tasks.runServer { - minecraftVersion('1.21.11') + minecraftVersion('26.1.1') } runPaper.folia.registerTask() From f288f0e2126d59f3751d435e9258c4e322d6db9c Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 28 Mar 2026 17:37:21 +0800 Subject: [PATCH 31/55] update actions version --- .github/workflows/Java-CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Java-CI.yml b/.github/workflows/Java-CI.yml index 99f8f1d..c056bdd 100644 --- a/.github/workflows/Java-CI.yml +++ b/.github/workflows/Java-CI.yml @@ -14,8 +14,8 @@ jobs: runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 with: distribution: "temurin" java-version: "17" From 9b5210ad11d1ac630997a6e0888c6cabb2848775 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 6 Apr 2026 12:57:39 +0800 Subject: [PATCH 32/55] some small fixes --- src/main/java/org/milkteamc/autotreechop/Config.java | 11 +---------- .../milkteamc/autotreechop/utils/TreeChopUtils.java | 11 +++-------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/Config.java b/src/main/java/org/milkteamc/autotreechop/Config.java index d39dfe5..ded9b7e 100644 --- a/src/main/java/org/milkteamc/autotreechop/Config.java +++ b/src/main/java/org/milkteamc/autotreechop/Config.java @@ -37,7 +37,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.bukkit.Material; @@ -278,16 +277,8 @@ private Set loadMaterialSet(String path) { } private Material parseMaterial(String name) { - Optional xMat = XMaterial.matchXMaterial(name); - if (xMat.isPresent()) { - Material mat = xMat.get().get(); - if (mat != null) { - return mat; - } - } - try { - return Material.getMaterial(name); + return XMaterial.matchXMaterial(name).map(XMaterial::get).orElse(null); } catch (Exception e) { plugin.getLogger().fine("Material not available in this version: " + name); return null; diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 880b618..98050d1 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -17,6 +17,7 @@ package org.milkteamc.autotreechop.utils; +import com.cryptomorin.xseries.XEnchantment; import com.cryptomorin.xseries.XMaterial; import com.cryptomorin.xseries.XSound; import java.util.*; @@ -24,7 +25,6 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; -import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.inventory.ItemStack; @@ -106,7 +106,7 @@ private static void applyToolDamage(ItemStack tool, Player player, int blocksBro private static int getUnbreakingLevel(ItemStack item) { if (item != null && item.hasItemMeta() && item.getItemMeta().hasEnchants()) { - return item.getEnchantmentLevel(Enchantment.UNBREAKING); + return item.getEnchantmentLevel(XEnchantment.UNBREAKING.get()); } return 0; } @@ -569,12 +569,7 @@ private boolean removeLeafBlock( if (config.getLeafRemovalDropItems()) { leafBlock.breakNaturally(); } else { - Material air = XMaterial.AIR.get(); - if (air != null) { - leafBlock.setType(air, false); - } else { - leafBlock.setType(Material.AIR, false); - } + leafBlock.setType(XMaterial.AIR.get(), false); } // Update daily blocks count if needed From 69dca2f5eba94a6be21c5a6f33452fc919c780bf Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 6 Apr 2026 13:25:26 +0800 Subject: [PATCH 33/55] add 26.1 support --- build.gradle | 7 ++++++- .../java/org/milkteamc/autotreechop/utils/EffectUtils.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7924f13..4eff18f 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,11 @@ repositories { name = 'papermc' url = 'https://repo.papermc.io/repository/maven-public/' } + + maven { + name = 'marcely-repo' + url = 'https://repo.marcely.de/repository/maven-public/' + } } dependencies { @@ -82,7 +87,7 @@ dependencies { implementation 'com.github.Anon8281:UniversalScheduler:0.1.7' implementation 'com.zaxxer:HikariCP:7.0.2' implementation 'org.bstats:bstats-bukkit:3.1.0' - implementation 'com.github.cryptomorin:XSeries:13.6.0' + implementation 'com.github.cryptomorin:XSeries-Fork:13.6.0' implementation 'dev.dejvokep:boosted-yaml:1.3.7' compileOnly 'me.clip:placeholderapi:2.12.2' diff --git a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java index 3151a95..efbccbb 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java @@ -86,7 +86,7 @@ public static void showLeafRemovalEffect(Player player, Block block) { .spawn(); // Falling leaf-like block particles - if (XMaterial.supports(13)) { + if (XMaterial.supports(1, 13)) { try { XMaterial blockMaterial = XMaterial.matchXMaterial(block.getType()); if (blockMaterial != null && blockMaterial.get() != null) { From 3f1c60f79e069e8a82872cbe27cc0a083c977119 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 6 Apr 2026 13:27:04 +0800 Subject: [PATCH 34/55] mark 26.1(.1) as supported --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4eff18f..5224940 100644 --- a/build.gradle +++ b/build.gradle @@ -212,7 +212,8 @@ modrinth { '1.19', '1.19.1', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.3', '1.20.4', '1.20.5', '1.20.6', '1.21', '1.21.1', '1.21.2', '1.21.3', '1.21.4', '1.21.5', '1.21.6', - '1.21.7', '1.21.8', '1.21.9', '1.21.10', '1.21.11' + '1.21.7', '1.21.8', '1.21.9', '1.21.10', '1.21.11', + '26.1', '26.1.1' ] loaders = ['paper', 'purpur', 'folia', 'spigot'] From 702ade3c7cea6ba4a9bd939fb73934d4c22041f5 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 6 Apr 2026 13:32:10 +0800 Subject: [PATCH 35/55] we can still use java 21 to build jar --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5224940..bfadc2d 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ version = '1.7.4' // Java Configuration java { toolchain { - languageVersion = JavaLanguageVersion.of(25) + languageVersion = JavaLanguageVersion.of(21) } } @@ -178,6 +178,9 @@ tasks.shadowJar { tasks.runServer { minecraftVersion('26.1.1') + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(25) + } } runPaper.folia.registerTask() From bde8a537121c24846abc8c3dc871fabee472c0a9 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 6 Apr 2026 14:03:38 +0800 Subject: [PATCH 36/55] fix enable/disable command logic --- .../milkteamc/autotreechop/AutoTreeChop.java | 2 + .../autotreechop/command/ToggleCommand.java | 93 +++++++++---------- src/main/resources/lang/en.properties | 3 + src/main/resources/lang/zh.properties | 3 + 4 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 3df1d6b..09b9094 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -76,6 +76,8 @@ public class AutoTreeChop extends JavaPlugin { public static final String CONFIRMATION_REQUIRED_BOTH_MESSAGE = "confirmationRequiredBoth"; public static final String CONFIRMATION_SUCCESS_MESSAGE = "confirmationSuccess"; public static final String NO_PENDING_CONFIRMATION_MESSAGE = "noPendingConfirmation"; + public static final String ALREADY_ENABLED_MESSAGE = "alreadyEnabled"; + public static final String ALREADY_DISABLED_MESSAGE = "alreadyDisabled"; public static final String ABOUT_HEADER = "aboutHeader"; public static final String ABOUT_LICENSE = "aboutLicense"; public static final String ABOUT_GITHUB = "aboutGithub"; diff --git a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java index 8cc4d5f..39f2486 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java @@ -83,37 +83,37 @@ public void toggle(BukkitCommandActor actor, @Optional Player targetPlayer) { } } - // NOTE: No @CommandPermission here — permission is checked manually below so that - // self-use requires autotreechop.use while targeting others requires autotreechop.other. + // enable — self (no args) @Subcommand("enable") - public void enable(BukkitCommandActor actor, @Optional EntitySelector targetPlayers) { - if (targetPlayers == null) { - if (!actor.sender().hasPermission("autotreechop.use")) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); - return; - } - if (!plugin.getPluginConfig().getCommandToggle()) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); - return; - } - if (!(actor.sender() instanceof Player player)) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); - return; - } - plugin.getPlayerConfig(player.getUniqueId()).setAutoTreeChopEnabled(true); - AutoTreeChop.sendMessage(player, AutoTreeChop.ENABLED_MESSAGE); + @CommandPermission("autotreechop.use") + public void enable(BukkitCommandActor actor) { + if (!plugin.getPluginConfig().getCommandToggle()) { + AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); return; } - - if (!actor.sender().hasPermission("autotreechop.other")) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); + if (!(actor.sender() instanceof Player player)) { + AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); + return; + } + PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); + if (playerConfig.isAutoTreeChopEnabled()) { + AutoTreeChop.sendMessage(player, AutoTreeChop.ALREADY_ENABLED_MESSAGE); return; } + playerConfig.setAutoTreeChopEnabled(true); + AutoTreeChop.sendMessage(player, AutoTreeChop.ENABLED_MESSAGE); + } + // enable — targets (requires .other) + @Subcommand("enable") + @CommandPermission("autotreechop.other") + public void enable(BukkitCommandActor actor, EntitySelector targetPlayers) { int count = 0; String lastName = null; for (Player targetPlayer : targetPlayers) { - plugin.getPlayerConfig(targetPlayer.getUniqueId()).setAutoTreeChopEnabled(true); + PlayerConfig cfg = plugin.getPlayerConfig(targetPlayer.getUniqueId()); + if (cfg.isAutoTreeChopEnabled()) continue; // skip already-enabled silently, or send per-player msg + cfg.setAutoTreeChopEnabled(true); lastName = targetPlayer.getName(); count++; AutoTreeChop.sendMessage( @@ -121,7 +121,6 @@ public void enable(BukkitCommandActor actor, @Optional EntitySelector ta AutoTreeChop.ENABLED_BY_OTHER_MESSAGE, Placeholder.parsed("player", actor.sender().getName())); } - if (count == 1 && lastName != null) { AutoTreeChop.sendMessage( actor.sender(), AutoTreeChop.ENABLED_FOR_OTHER_MESSAGE, Placeholder.parsed("player", lastName)); @@ -131,39 +130,40 @@ public void enable(BukkitCommandActor actor, @Optional EntitySelector ta } } - // NOTE: Same reasoning as enable — no @CommandPermission; checked manually below. + // disable — self @Subcommand("disable") - public void disable(BukkitCommandActor actor, @Optional EntitySelector targetPlayers) { - if (targetPlayers == null) { - if (!actor.sender().hasPermission("autotreechop.use")) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); - return; - } - if (!plugin.getPluginConfig().getCommandToggle()) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); - return; - } - if (!(actor.sender() instanceof Player player)) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); - return; - } - UUID playerUUID = player.getUniqueId(); - plugin.getPlayerConfig(playerUUID).setAutoTreeChopEnabled(false); - plugin.getConfirmationManager().clearPlayer(playerUUID); - AutoTreeChop.sendMessage(player, AutoTreeChop.DISABLED_MESSAGE); + @CommandPermission("autotreechop.use") + public void disable(BukkitCommandActor actor) { + if (!plugin.getPluginConfig().getCommandToggle()) { + AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); return; } - - if (!actor.sender().hasPermission("autotreechop.other")) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); + if (!(actor.sender() instanceof Player player)) { + AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); return; } + UUID playerUUID = player.getUniqueId(); + PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); + if (!playerConfig.isAutoTreeChopEnabled()) { + AutoTreeChop.sendMessage(player, AutoTreeChop.ALREADY_DISABLED_MESSAGE); + return; + } + playerConfig.setAutoTreeChopEnabled(false); + plugin.getConfirmationManager().clearPlayer(playerUUID); + AutoTreeChop.sendMessage(player, AutoTreeChop.DISABLED_MESSAGE); + } + // disable — targets + @Subcommand("disable") + @CommandPermission("autotreechop.other") + public void disable(BukkitCommandActor actor, EntitySelector targetPlayers) { int count = 0; String lastName = null; for (Player targetPlayer : targetPlayers) { UUID targetUUID = targetPlayer.getUniqueId(); - plugin.getPlayerConfig(targetUUID).setAutoTreeChopEnabled(false); + PlayerConfig cfg = plugin.getPlayerConfig(targetUUID); + if (!cfg.isAutoTreeChopEnabled()) continue; + cfg.setAutoTreeChopEnabled(false); plugin.getConfirmationManager().clearPlayer(targetUUID); lastName = targetPlayer.getName(); count++; @@ -172,7 +172,6 @@ public void disable(BukkitCommandActor actor, @Optional EntitySelector t AutoTreeChop.DISABLED_BY_OTHER_MESSAGE, Placeholder.parsed("player", actor.sender().getName())); } - if (count == 1 && lastName != null) { AutoTreeChop.sendMessage( actor.sender(), AutoTreeChop.DISABLED_FOR_OTHER_MESSAGE, Placeholder.parsed("player", lastName)); diff --git a/src/main/resources/lang/en.properties b/src/main/resources/lang/en.properties index af3d21e..3c1f47c 100644 --- a/src/main/resources/lang/en.properties +++ b/src/main/resources/lang/en.properties @@ -30,6 +30,9 @@ noPendingConfirmation=There is no pending AutoTreeChop confirmation. sneakEnabled=AutoTreeChop enabled while sneaking. sneakDisabled=AutoTreeChop disabled after stopping sneak. +alreadyEnabled=AutoTreeChop is already enabled. +alreadyDisabled=AutoTreeChop is already disabled. + consoleName=console aboutHeader=AutoTreeChop - v{version} by the MilkTeaMC team and contributors diff --git a/src/main/resources/lang/zh.properties b/src/main/resources/lang/zh.properties index c0708eb..ba40d33 100644 --- a/src/main/resources/lang/zh.properties +++ b/src/main/resources/lang/zh.properties @@ -30,6 +30,9 @@ noPendingConfirmation=目前沒有待確認的自動砍樹操作。 sneakEnabled=因進入潛行狀態,自動砍樹已啟用。 sneakDisabled=已離開潛行狀態,自動砍樹已停用。 +alreadyEnabled=自動砍樹已經是啟用狀態。 +alreadyDisabled=自動砍樹已經是停用狀態。 + consoleName=控制台 aboutHeader=AutoTreeChop - v{version} 由 MilkTeaMC 團隊與貢獻者開發 From d004f086026de1a286d28f9e254d2e2b0ad6d5ea Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Mon, 6 Apr 2026 20:24:29 +0800 Subject: [PATCH 37/55] move message keys to a class --- .../milkteamc/autotreechop/AutoTreeChop.java | 30 ----------- .../milkteamc/autotreechop/MessageKeys.java | 52 +++++++++++++++++++ .../autotreechop/command/AboutCommand.java | 9 ++-- .../autotreechop/command/ConfirmCommand.java | 9 ++-- .../autotreechop/command/ToggleCommand.java | 47 +++++++++-------- .../autotreechop/command/UsageCommand.java | 7 +-- .../events/BlockBreakListener.java | 13 ++--- .../events/PlayerSneakListener.java | 5 +- .../autotreechop/utils/EffectUtils.java | 7 ++- .../autotreechop/utils/TreeChopUtils.java | 5 +- 10 files changed, 106 insertions(+), 78 deletions(-) create mode 100644 src/main/java/org/milkteamc/autotreechop/MessageKeys.java diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index 09b9094..b9849d3 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -53,36 +53,6 @@ public class AutoTreeChop extends JavaPlugin { - // Message keys (replacing old Message objects) - public static final String NO_RESIDENCE_PERMISSIONS = "noResidencePermissions"; - public static final String ENABLED_MESSAGE = "enabled"; - public static final String DISABLED_MESSAGE = "disabled"; - public static final String NO_PERMISSION_MESSAGE = "no-permission"; - public static final String ONLY_PLAYERS_MESSAGE = "only-players"; - public static final String HIT_MAX_USAGE_MESSAGE = "hitmaxusage"; - public static final String HIT_MAX_BLOCK_MESSAGE = "hitmaxblock"; - public static final String USAGE_MESSAGE = "usage"; - public static final String BLOCKS_BROKEN_MESSAGE = "blocks-broken"; - public static final String ENABLED_BY_OTHER_MESSAGE = "enabledByOther"; - public static final String ENABLED_FOR_OTHER_MESSAGE = "enabledForOther"; - public static final String DISABLED_BY_OTHER_MESSAGE = "disabledByOther"; - public static final String DISABLED_FOR_OTHER_MESSAGE = "disabledForOther"; - public static final String STILL_IN_COOLDOWN_MESSAGE = "stillInCooldown"; - public static final String CONSOLE_NAME = "consoleName"; - public static final String SNEAK_ENABLED_MESSAGE = "sneakEnabled"; - public static final String SNEAK_DISABLED_MESSAGE = "sneakDisabled"; - public static final String CONFIRMATION_REQUIRED_IDLE_MESSAGE = "confirmationRequiredIdle"; - public static final String CONFIRMATION_REQUIRED_NO_LEAVES_MESSAGE = "confirmationRequiredNoLeaves"; - public static final String CONFIRMATION_REQUIRED_BOTH_MESSAGE = "confirmationRequiredBoth"; - public static final String CONFIRMATION_SUCCESS_MESSAGE = "confirmationSuccess"; - public static final String NO_PENDING_CONFIRMATION_MESSAGE = "noPendingConfirmation"; - public static final String ALREADY_ENABLED_MESSAGE = "alreadyEnabled"; - public static final String ALREADY_DISABLED_MESSAGE = "alreadyDisabled"; - public static final String ABOUT_HEADER = "aboutHeader"; - public static final String ABOUT_LICENSE = "aboutLicense"; - public static final String ABOUT_GITHUB = "aboutGithub"; - public static final String ABOUT_MODRINTH = "aboutModrinth"; - private static final long SAVE_INTERVAL = 1200L; // 60s private static final int SAVE_THRESHOLD = 15; diff --git a/src/main/java/org/milkteamc/autotreechop/MessageKeys.java b/src/main/java/org/milkteamc/autotreechop/MessageKeys.java new file mode 100644 index 0000000..3eb53e7 --- /dev/null +++ b/src/main/java/org/milkteamc/autotreechop/MessageKeys.java @@ -0,0 +1,52 @@ +/* + * 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; + +public final class MessageKeys { + + private MessageKeys() {} + + public static final String NO_RESIDENCE_PERMISSIONS = "noResidencePermissions"; + public static final String ENABLED = "enabled"; + public static final String DISABLED = "disabled"; + public static final String NO_PERMISSION = "no-permission"; + public static final String ONLY_PLAYERS = "only-players"; + public static final String HIT_MAX_USAGE = "hitmaxusage"; + public static final String HIT_MAX_BLOCK = "hitmaxblock"; + public static final String USAGE = "usage"; + public static final String BLOCKS_BROKEN = "blocks-broken"; + public static final String ENABLED_BY_OTHER = "enabledByOther"; + public static final String ENABLED_FOR_OTHER = "enabledForOther"; + public static final String DISABLED_BY_OTHER = "disabledByOther"; + public static final String DISABLED_FOR_OTHER = "disabledForOther"; + public static final String STILL_IN_COOLDOWN = "stillInCooldown"; + public static final String CONSOLE_NAME = "consoleName"; + public static final String SNEAK_ENABLED = "sneakEnabled"; + public static final String SNEAK_DISABLED = "sneakDisabled"; + public static final String CONFIRMATION_REQUIRED_IDLE = "confirmationRequiredIdle"; + public static final String CONFIRMATION_REQUIRED_NO_LEAVES = "confirmationRequiredNoLeaves"; + public static final String CONFIRMATION_REQUIRED_BOTH = "confirmationRequiredBoth"; + public static final String CONFIRMATION_SUCCESS = "confirmationSuccess"; + public static final String NO_PENDING_CONFIRMATION = "noPendingConfirmation"; + public static final String ALREADY_ENABLED = "alreadyEnabled"; + public static final String ALREADY_DISABLED = "alreadyDisabled"; + public static final String ABOUT_HEADER = "aboutHeader"; + public static final String ABOUT_LICENSE = "aboutLicense"; + public static final String ABOUT_GITHUB = "aboutGithub"; + public static final String ABOUT_MODRINTH = "aboutModrinth"; +} diff --git a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java index 3c3bb82..8ea8b89 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java @@ -20,6 +20,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import org.bukkit.command.CommandSender; import org.milkteamc.autotreechop.AutoTreeChop; +import org.milkteamc.autotreechop.MessageKeys; import revxrsal.commands.annotation.Command; import revxrsal.commands.annotation.Subcommand; import revxrsal.commands.bukkit.actor.BukkitCommandActor; @@ -39,11 +40,11 @@ public void about(BukkitCommandActor actor) { AutoTreeChop.sendMessage( sender, - AutoTreeChop.ABOUT_HEADER, + MessageKeys.ABOUT_HEADER, Placeholder.parsed("version", plugin.getDescription().getVersion())); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_LICENSE); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_GITHUB); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_MODRINTH); + AutoTreeChop.sendMessage(sender, MessageKeys.ABOUT_LICENSE); + AutoTreeChop.sendMessage(sender, MessageKeys.ABOUT_GITHUB); + AutoTreeChop.sendMessage(sender, MessageKeys.ABOUT_MODRINTH); } } diff --git a/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java index bdd614d..e488ec3 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ConfirmCommand.java @@ -22,6 +22,7 @@ import org.bukkit.entity.Player; import org.milkteamc.autotreechop.AutoTreeChop; import org.milkteamc.autotreechop.Config; +import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; import org.milkteamc.autotreechop.utils.BlockDiscoveryUtils; import org.milkteamc.autotreechop.utils.ConfirmationManager.ChopData; @@ -45,7 +46,7 @@ public ConfirmCommand(AutoTreeChop plugin) { @CommandPermission("autotreechop.use") public void confirm(BukkitCommandActor actor) { if (!(actor.sender() instanceof Player player)) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.ONLY_PLAYERS); return; } @@ -57,7 +58,7 @@ public void confirm(BukkitCommandActor actor) { ChopData chop = plugin.getConfirmationManager().consumePendingConfirmation(uuid); if (chop == null) { - AutoTreeChop.sendMessage(player, AutoTreeChop.NO_PENDING_CONFIRMATION_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.NO_PENDING_CONFIRMATION); return; } @@ -70,12 +71,12 @@ public void confirm(BukkitCommandActor actor) { if (!BlockDiscoveryUtils.isLog(block.getType(), config)) { // Log is gone — treat as if there was no pending confirmation so the // player gets clear feedback rather than a silent no-op. - AutoTreeChop.sendMessage(player, AutoTreeChop.NO_PENDING_CONFIRMATION_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.NO_PENDING_CONFIRMATION); return; } plugin.getConfirmationManager().recordSuccessfulChop(uuid, chop.reason(), false); - AutoTreeChop.sendMessage(player, AutoTreeChop.CONFIRMATION_SUCCESS_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.CONFIRMATION_SUCCESS); if (config.isVisualEffect()) { EffectUtils.showChopEffect(player, block); diff --git a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java index 39f2486..a564e55 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java @@ -21,6 +21,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import org.bukkit.entity.Player; import org.milkteamc.autotreechop.AutoTreeChop; +import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; import revxrsal.commands.annotation.Command; import revxrsal.commands.annotation.Optional; @@ -52,7 +53,7 @@ public void toggle(BukkitCommandActor actor, @Optional Player targetPlayer) { } if (!actor.sender().hasPermission("autotreechop.other")) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.NO_PERMISSION); return; } @@ -64,21 +65,21 @@ public void toggle(BukkitCommandActor actor, @Optional Player targetPlayer) { if (autoTreeChopEnabled) { AutoTreeChop.sendMessage( targetPlayer, - AutoTreeChop.ENABLED_BY_OTHER_MESSAGE, + MessageKeys.ENABLED_BY_OTHER, Placeholder.parsed("player", actor.sender().getName())); AutoTreeChop.sendMessage( actor.sender(), - AutoTreeChop.ENABLED_FOR_OTHER_MESSAGE, + MessageKeys.ENABLED_FOR_OTHER, Placeholder.parsed("player", targetPlayer.getName())); } else { plugin.getConfirmationManager().clearPlayer(targetUUID); AutoTreeChop.sendMessage( targetPlayer, - AutoTreeChop.DISABLED_BY_OTHER_MESSAGE, + MessageKeys.DISABLED_BY_OTHER, Placeholder.parsed("player", actor.sender().getName())); AutoTreeChop.sendMessage( actor.sender(), - AutoTreeChop.DISABLED_FOR_OTHER_MESSAGE, + MessageKeys.DISABLED_FOR_OTHER, Placeholder.parsed("player", targetPlayer.getName())); } } @@ -88,20 +89,20 @@ public void toggle(BukkitCommandActor actor, @Optional Player targetPlayer) { @CommandPermission("autotreechop.use") public void enable(BukkitCommandActor actor) { if (!plugin.getPluginConfig().getCommandToggle()) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.NO_PERMISSION); return; } if (!(actor.sender() instanceof Player player)) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.ONLY_PLAYERS); return; } PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); if (playerConfig.isAutoTreeChopEnabled()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.ALREADY_ENABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.ALREADY_ENABLED); return; } playerConfig.setAutoTreeChopEnabled(true); - AutoTreeChop.sendMessage(player, AutoTreeChop.ENABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.ENABLED); } // enable — targets (requires .other) @@ -118,15 +119,15 @@ public void enable(BukkitCommandActor actor, EntitySelector targetPlayer count++; AutoTreeChop.sendMessage( targetPlayer, - AutoTreeChop.ENABLED_BY_OTHER_MESSAGE, + MessageKeys.ENABLED_BY_OTHER, Placeholder.parsed("player", actor.sender().getName())); } if (count == 1 && lastName != null) { AutoTreeChop.sendMessage( - actor.sender(), AutoTreeChop.ENABLED_FOR_OTHER_MESSAGE, Placeholder.parsed("player", lastName)); + actor.sender(), MessageKeys.ENABLED_FOR_OTHER, Placeholder.parsed("player", lastName)); } else if (count > 1) { AutoTreeChop.sendMessage( - actor.sender(), AutoTreeChop.ENABLED_FOR_OTHER_MESSAGE, Placeholder.parsed("player", "everyone")); + actor.sender(), MessageKeys.ENABLED_FOR_OTHER, Placeholder.parsed("player", "everyone")); } } @@ -135,22 +136,22 @@ public void enable(BukkitCommandActor actor, EntitySelector targetPlayer @CommandPermission("autotreechop.use") public void disable(BukkitCommandActor actor) { if (!plugin.getPluginConfig().getCommandToggle()) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.NO_PERMISSION); return; } if (!(actor.sender() instanceof Player player)) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.ONLY_PLAYERS); return; } UUID playerUUID = player.getUniqueId(); PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); if (!playerConfig.isAutoTreeChopEnabled()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.ALREADY_DISABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.ALREADY_DISABLED); return; } playerConfig.setAutoTreeChopEnabled(false); plugin.getConfirmationManager().clearPlayer(playerUUID); - AutoTreeChop.sendMessage(player, AutoTreeChop.DISABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.DISABLED); } // disable — targets @@ -169,26 +170,26 @@ public void disable(BukkitCommandActor actor, EntitySelector targetPlaye count++; AutoTreeChop.sendMessage( targetPlayer, - AutoTreeChop.DISABLED_BY_OTHER_MESSAGE, + MessageKeys.DISABLED_BY_OTHER, Placeholder.parsed("player", actor.sender().getName())); } if (count == 1 && lastName != null) { AutoTreeChop.sendMessage( - actor.sender(), AutoTreeChop.DISABLED_FOR_OTHER_MESSAGE, Placeholder.parsed("player", lastName)); + actor.sender(), MessageKeys.DISABLED_FOR_OTHER, Placeholder.parsed("player", lastName)); } else if (count > 1) { AutoTreeChop.sendMessage( - actor.sender(), AutoTreeChop.DISABLED_FOR_OTHER_MESSAGE, Placeholder.parsed("player", "everyone")); + actor.sender(), MessageKeys.DISABLED_FOR_OTHER, Placeholder.parsed("player", "everyone")); } } private void performSelfToggle(BukkitCommandActor actor) { if (!(actor.sender() instanceof Player player)) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.ONLY_PLAYERS); return; } if (!plugin.getPluginConfig().getCommandToggle()) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.NO_PERMISSION_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.NO_PERMISSION); return; } @@ -198,10 +199,10 @@ private void performSelfToggle(BukkitCommandActor actor) { playerConfig.setAutoTreeChopEnabled(autoTreeChopEnabled); if (autoTreeChopEnabled) { - AutoTreeChop.sendMessage(player, AutoTreeChop.ENABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.ENABLED); } else { plugin.getConfirmationManager().clearPlayer(playerUUID); - AutoTreeChop.sendMessage(player, AutoTreeChop.DISABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.DISABLED); } } } diff --git a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java index 184b971..46a0956 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java @@ -21,6 +21,7 @@ import org.bukkit.entity.Player; import org.milkteamc.autotreechop.AutoTreeChop; import org.milkteamc.autotreechop.Config; +import org.milkteamc.autotreechop.MessageKeys; import revxrsal.commands.annotation.Command; import revxrsal.commands.annotation.Subcommand; import revxrsal.commands.bukkit.actor.BukkitCommandActor; @@ -41,7 +42,7 @@ public UsageCommand(AutoTreeChop plugin, Config config) { @CommandPermission("autotreechop.use") public void usage(BukkitCommandActor actor) { if (!actor.isPlayer()) { - AutoTreeChop.sendMessage(actor.sender(), AutoTreeChop.ONLY_PLAYERS_MESSAGE); + AutoTreeChop.sendMessage(actor.sender(), MessageKeys.ONLY_PLAYERS); return; } @@ -67,13 +68,13 @@ public void usage(BukkitCommandActor actor) { AutoTreeChop.sendMessage( player, - AutoTreeChop.USAGE_MESSAGE, + MessageKeys.USAGE, Placeholder.parsed("current_uses", String.valueOf(pConfig.getDailyUses())), Placeholder.parsed("max_uses", maxUsesStr)); AutoTreeChop.sendMessage( player, - AutoTreeChop.BLOCKS_BROKEN_MESSAGE, + MessageKeys.BLOCKS_BROKEN, Placeholder.parsed("current_blocks", String.valueOf(pConfig.getDailyBlocksBroken())), Placeholder.parsed("max_blocks", maxBlocksStr)); } diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 7405fba..b142913 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -36,6 +36,7 @@ import org.bukkit.inventory.ItemStack; import org.milkteamc.autotreechop.AutoTreeChop; import org.milkteamc.autotreechop.Config; +import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; import org.milkteamc.autotreechop.utils.AsyncTaskScheduler; import org.milkteamc.autotreechop.utils.BlockDiscoveryUtils; @@ -103,7 +104,7 @@ public void onBlockBreak(BlockBreakEvent event) { long remaining = plugin.getCooldownManager().getRemainingCooldown(playerUUID); AutoTreeChop.sendMessage( player, - AutoTreeChop.STILL_IN_COOLDOWN_MESSAGE, + MessageKeys.STILL_IN_COOLDOWN, Placeholder.parsed("cooldown_time", String.valueOf(remaining))); return; } @@ -116,7 +117,7 @@ public void onBlockBreak(BlockBreakEvent event) { if (!PermissionUtils.hasVipUses(player, playerConfig, config) && playerConfig.getDailyUses() >= config.getMaxUsesPerDay()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.HIT_MAX_USAGE_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.HIT_MAX_USAGE); return; } @@ -128,7 +129,7 @@ public void onBlockBreak(BlockBreakEvent event) { // 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, AutoTreeChop.CONFIRMATION_SUCCESS_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.CONFIRMATION_SUCCESS); dispatchChop(player, playerConfig, block, tool, location, config); return; } @@ -168,9 +169,9 @@ public void onBlockBreak(BlockBreakEvent event) { String timeoutStr = String.valueOf(config.getConfirmationWindowSeconds()); String messageKey = switch (reason) { - case IDLE_OR_REJOIN -> AutoTreeChop.CONFIRMATION_REQUIRED_IDLE_MESSAGE; - case NO_LEAVES -> AutoTreeChop.CONFIRMATION_REQUIRED_NO_LEAVES_MESSAGE; - case BOTH -> AutoTreeChop.CONFIRMATION_REQUIRED_BOTH_MESSAGE; + case IDLE_OR_REJOIN -> MessageKeys.CONFIRMATION_REQUIRED_IDLE; + case NO_LEAVES -> MessageKeys.CONFIRMATION_REQUIRED_NO_LEAVES; + case BOTH -> MessageKeys.CONFIRMATION_REQUIRED_BOTH; }; AutoTreeChop.sendMessage(player, messageKey, Placeholder.parsed("timeout", timeoutStr)); return; diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java index 5c895a2..7fc1c2a 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerSneakListener.java @@ -23,6 +23,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerToggleSneakEvent; import org.milkteamc.autotreechop.AutoTreeChop; +import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; public class PlayerSneakListener implements Listener { @@ -47,12 +48,12 @@ public void onPlayerToggleSneak(PlayerToggleSneakEvent event) { if (event.isSneaking()) { playerConfig.setAutoTreeChopEnabled(true); if (plugin.getPluginConfig().getSneakMessage()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.SNEAK_ENABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.SNEAK_ENABLED); } } else { playerConfig.setAutoTreeChopEnabled(false); if (plugin.getPluginConfig().getSneakMessage()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.SNEAK_DISABLED_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.SNEAK_DISABLED); } } } diff --git a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java index 3151a95..b744bbb 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java @@ -17,9 +17,6 @@ package org.milkteamc.autotreechop.utils; -import static org.milkteamc.autotreechop.AutoTreeChop.HIT_MAX_BLOCK_MESSAGE; -import static org.milkteamc.autotreechop.AutoTreeChop.sendMessage; - import com.cryptomorin.xseries.XMaterial; import com.cryptomorin.xseries.particles.ParticleDisplay; import com.cryptomorin.xseries.particles.XParticle; @@ -27,13 +24,15 @@ import java.util.logging.Logger; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import org.milkteamc.autotreechop.AutoTreeChop; +import org.milkteamc.autotreechop.MessageKeys; public class EffectUtils { private static final Logger LOGGER = Logger.getLogger("AutoTreeChop"); public static void sendMaxBlockLimitReachedMessage(Player player, Block block) { - sendMessage(player, HIT_MAX_BLOCK_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.HIT_MAX_BLOCK); ParticleDisplay.of(XParticle.DUST) .withLocation(block.getLocation().add(0.5, 0.5, 0.5)) .withColor(Color.RED, 1.0f) diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java index 98050d1..9cbde37 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -31,6 +31,7 @@ import org.bukkit.inventory.meta.Damageable; import org.milkteamc.autotreechop.AutoTreeChop; import org.milkteamc.autotreechop.Config; +import org.milkteamc.autotreechop.MessageKeys; import org.milkteamc.autotreechop.PlayerConfig; public class TreeChopUtils { @@ -241,14 +242,14 @@ private void validateAndExecuteChop( } if (treeBlocks.size() > config.getMaxTreeSize()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.HIT_MAX_BLOCK_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.HIT_MAX_BLOCK); sessionManager.clearTreeChopSession(playerUUID); return; } if (!PermissionUtils.hasVipBlock(player, playerConfig, config)) { if (playerConfig.getDailyBlocksBroken() + treeBlocks.size() > config.getMaxBlocksPerDay()) { - AutoTreeChop.sendMessage(player, AutoTreeChop.HIT_MAX_BLOCK_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.HIT_MAX_BLOCK); sessionManager.clearTreeChopSession(playerUUID); return; } From 163c7a8ec457dda371aa4cb61cbbd3bd2a49dd8a Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 6 Apr 2026 07:59:23 +0000 Subject: [PATCH 38/55] [ci skip] Translated using Weblate (Spanish) Currently translated at 100.0% (28 of 28 strings) Translation: AutoTreeChop/AutoTreeChop Translate-URL: https://translate.codeberg.org/projects/autotreechop/autotreechop/es/ --- src/main/resources/lang/es.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/lang/es.properties b/src/main/resources/lang/es.properties index 665ca73..3563171 100644 --- a/src/main/resources/lang/es.properties +++ b/src/main/resources/lang/es.properties @@ -24,3 +24,5 @@ aboutHeader=AutoTreeChop - v{versión}Licencia: GNU General Public License v3.0 (GPL-3.0) aboutGithub=GitHub: https://github.com/milkteamc/autotreechop aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop +alreadyEnabled=AutoTreeChop está activado. +alreadyDisabled=AutoTreeChop está desactivado. From 460b67f2b6347a07ea0f02acc213d4e9fab8f3f6 Mon Sep 17 00:00:00 2001 From: Metr Date: Tue, 7 Apr 2026 20:00:14 +0300 Subject: [PATCH 39/55] Revert "[fix] remove deprecated method of getting plugin metadata" This reverts commit 66566f5fe029686467b5e79d6b3863f0bcc840e1. --- .../java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java index 1f7a5de..0e10e19 100644 --- a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java +++ b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java @@ -90,7 +90,7 @@ public enum UpdateCheckResult { public ModrinthUpdateChecker(@NotNull JavaPlugin plugin, @NotNull String projectId, @NotNull String loader) { this.plugin = plugin; this.projectId = projectId; - this.currentVersion = plugin.getPluginMeta().getVersion(); + this.currentVersion = plugin.getDescription().getVersion(); this.loader = loader; this.minecraftVersion = plugin.getServer().getBukkitVersion().split("-")[0]; } From 61648174f51838236c7a90365c9b0eec3c44123d Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Wed, 8 Apr 2026 22:16:33 +0800 Subject: [PATCH 40/55] use another source of XSeries --- build.gradle | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index bfadc2d..26d5954 100644 --- a/build.gradle +++ b/build.gradle @@ -68,11 +68,6 @@ repositories { name = 'papermc' url = 'https://repo.papermc.io/repository/maven-public/' } - - maven { - name = 'marcely-repo' - url = 'https://repo.marcely.de/repository/maven-public/' - } } dependencies { @@ -87,7 +82,7 @@ dependencies { implementation 'com.github.Anon8281:UniversalScheduler:0.1.7' implementation 'com.zaxxer:HikariCP:7.0.2' implementation 'org.bstats:bstats-bukkit:3.1.0' - implementation 'com.github.cryptomorin:XSeries-Fork:13.6.0' + implementation 'io.github.almighty-satan:XSeries:13.6.0+26.1' implementation 'dev.dejvokep:boosted-yaml:1.3.7' compileOnly 'me.clip:placeholderapi:2.12.2' From c38b6ecf8f8db3d1347ad11d0132c7887de71658 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Fri, 10 Apr 2026 23:01:32 +0800 Subject: [PATCH 41/55] this version checking is useless --- .../autotreechop/utils/EffectUtils.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java index 502a5f9..7352448 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java @@ -85,25 +85,22 @@ public static void showLeafRemovalEffect(Player player, Block block) { .spawn(); // Falling leaf-like block particles - if (XMaterial.supports(1, 13)) { - try { - XMaterial blockMaterial = XMaterial.matchXMaterial(block.getType()); - if (blockMaterial != null && blockMaterial.get() != null) { - ParticleDisplay.of(XParticle.BLOCK) - .withLocation(block.getLocation().add(0.5, 0.8, 0.5)) - .withBlock(blockMaterial.get().createBlockData()) - .withCount(10) - .offset(0.2, 0.1, 0.2) - .spawn(); - } - } catch (NoSuchMethodError | UnsupportedOperationException e) { - // The BLOCK particle API changed between MC versions; XSeries could not - // provide a compatible implementation on this server. The visual is - // purely cosmetic so we degrade gracefully, but log at FINE so server - // admins can diagnose version-compatibility issues if needed. - LOGGER.fine( - "BLOCK particle unavailable for leaf removal effect on this server version: " + e.getMessage()); + try { + XMaterial blockMaterial = XMaterial.matchXMaterial(block.getType()); + if (blockMaterial != null && blockMaterial.get() != null) { + ParticleDisplay.of(XParticle.BLOCK) + .withLocation(block.getLocation().add(0.5, 0.8, 0.5)) + .withBlock(blockMaterial.get().createBlockData()) + .withCount(10) + .offset(0.2, 0.1, 0.2) + .spawn(); } + } catch (NoSuchMethodError | UnsupportedOperationException e) { + // The BLOCK particle API changed between MC versions; XSeries could not + // provide a compatible implementation on this server. The visual is + // purely cosmetic so we degrade gracefully, but log at FINE so server + // admins can diagnose version-compatibility issues if needed. + LOGGER.fine("BLOCK particle unavailable for leaf removal effect on this server version: " + e.getMessage()); } } } From 2df2aa7f3d8eca007d93cecb7ca0b73e1cf0bc76 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Fri, 10 Apr 2026 23:02:01 +0800 Subject: [PATCH 42/55] arch update my jdk to 26 :< --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 26d5954..decd681 100644 --- a/build.gradle +++ b/build.gradle @@ -173,8 +173,9 @@ tasks.shadowJar { tasks.runServer { minecraftVersion('26.1.1') + def javaVersion = Math.max(25, JavaVersion.current().majorVersion.toInteger()) javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(25) + languageVersion = JavaLanguageVersion.of(javaVersion) } } From ee745af9c3db7d3f6fcc2158c58cc859faa701ea Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Fri, 10 Apr 2026 23:09:17 +0800 Subject: [PATCH 43/55] allow plugin to run on java 17 env --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 219dcf4..0ef96ff 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ version = '1.7.4' // Java Configuration java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } @@ -71,7 +71,7 @@ repositories { } dependencies { - compileOnly 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT' implementation 'net.kyori:adventure-platform-bukkit:4.4.1' implementation 'net.kyori:adventure-text-minimessage:4.26.1' @@ -88,7 +88,7 @@ dependencies { compileOnly 'me.clip:placeholderapi:2.12.2' compileOnly files('./libs/Residence5.1.4.3.jar') compileOnly 'com.github.angeschossen:LandsAPI:7.17.2' - compileOnly 'com.sk89q.worldguard:worldguard-bukkit:7.0.14' + compileOnly 'com.sk89q.worldguard:worldguard-bukkit:7.0.9' compileOnly 'com.github.GriefPrevention:GriefPrevention:16.18.4' } From d7db747f20d2670ef123de07cce1239771c4c1a3 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Fri, 10 Apr 2026 23:11:47 +0800 Subject: [PATCH 44/55] 26.1.2 should be supported --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0f8043d..d1cf045 100644 --- a/build.gradle +++ b/build.gradle @@ -212,7 +212,7 @@ modrinth { '1.20', '1.20.1', '1.20.2', '1.20.3', '1.20.4', '1.20.5', '1.20.6', '1.21', '1.21.1', '1.21.2', '1.21.3', '1.21.4', '1.21.5', '1.21.6', '1.21.7', '1.21.8', '1.21.9', '1.21.10', '1.21.11', - '26.1', '26.1.1' + '26.1', '26.1.1', '26.1.2' ] loaders = ['paper', 'purpur', 'folia', 'spigot'] From 977ca99c040bd4390a0f9ee000ff416b7ec0c63c Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 20:11:48 +0800 Subject: [PATCH 45/55] update gradle and some deps --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index d1cf045..b46a173 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { id 'java' id 'maven-publish' - id 'com.gradleup.shadow' version '9.3.1' - id 'xyz.jpenilla.run-paper' version '3.0.2' + id 'com.gradleup.shadow' version '9.4.1' + id 'xyz.jpenilla.run-paper' version '3.+' id 'com.modrinth.minotaur' version '2.+' id 'com.diffplug.spotless' version '8.+' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 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

SdO`o?U^bCPz6Ehh!k z$iWoy5;(rkuX8fgr;aa6Ctk;PqW79jwVr`$<8zxzba{NypJ@$l z5=}YGNTKY^CVPMFv|izZt(=n~ZASUrdGcrj2!jR42b+d`u4%~U9RMHcYj6;slDq%v#!)Pb zb~FxQVGheju_D^oGmIvUuFT<>>Q?^C;kaR(FoZ=poVf?pDinENSf?1hjyip!#r zz%VYqx3z!D-x{n9)>eHUhlb4B;Hqe3mR7nd6bSL_Bi)w<)$XyULxG4HGVjDS3i*#u zD(u41^0iB`Z7(A~>VLC1BoyeW{_HSrp_zGKW7E%=rA73;mL@Z!!JT+#Mq5aaad(Y7Vc|`7A-P* zs-LDsBltrOf2w}|fLXteeaC6R(?huUu)j*dUr7e_*<-* z-Clo^2&zi9qmeQRaP>$O? zd!qgt73?ajQM0?sTPt#EUTsBB*RVP$rxr48a%#ygWW*7j;)aM3;!=I}!#(ubqalNi z7*$J2H>{S?ollZr9~wdxHR{NSS#}SMXrzDAA2Pb=?#i56!C*esxf^r&TO^ED@?(B@ zM78D=jen7t85S7chr>c;MK_iA)TrYpWkyruikw>8tuIiV;O(8^+eg*OQMnDnYTbSE zosVseYSU-`RHIHU1eg0*g=_d;cn9vn&78ai-o|lS;1EYtf#1b`4Ije88vcRLx#Xt*_H{}a0437VjmMIokn22I!?nA)kY1IYEV6mr__b&3JtGRS7~^)x>3WM z)QE<6t4B3_R6VAi1=JJj=Nf-jJulFAmG650Y}KM+K!trb`97y{fr8)S`;x{53Vy3^ zkH!TGKH?kIxIn@0_1&*=fr3Ba+oykVfr3Bi`<2E83jVb3IgJYx`~}}j8W$+|%f44M zE>Q6Q`YSXpkhs6vzd&#eiNmK(W7)kNb^pUT29_DaaM8!Sicc`!mw;8JCHTX#-&K#%VR)Go<*wT%b@r|<54RLvX;}sk> z#tvP^K3yQ>NGac)`BXZvp{Qdw-`rkcEBdGc@OC>Sew-$4Jn=sdR9_IOCsP^@v#&<5=K#u+X1C$Ums% z`1RP~|36Sm2MA9re$SKMfM|bLYdzg~w+f!RF5;=Ecq52{A}9!6rn}Q^Gl=U#%m_R_Je)V~+@=g}C<)yiH)y$aH%Q}5 zX_>1u@!~Wj)(vTrmUy!*trxT@xUrqsx;rhYE!EvD@?x2Js_@usZpnXeYnxfq_?d5Y zv}VD!rMJc{C6P*qj7lO_yJRe%#d>3PeYN3*)OGKNAOtEGX~zU~s5A*Iq$ctsBSTI8 zt&v$q#y?JMF14Qj&IiTC%IFsuzm{F;Ynep;S@W8Lyo^Ei`x-w=WA+<6=`kwx3;$gf zT2kqbp;NL}Modhc{JLmdO9u#)3d59>tb$U1d|TCd|4#I{lB_&z$4Nv2xv^tnOO~C4#tsTE#|hwA zd0^*(NJ_YtuI)=CU7>pw$Giq>*gDwO(XzEkS73C^Y-L@ufgGAbVC#Ug(XM-UV{{ws z9xYuvwr+zBy#IIZl`T6mbX|V=>D=#}?|kPw-}nC>$FIEi#pj6VL*h<(z&Lfl{(TZX%}Om`1>i(4!EM@rc&Caf_nz6qqBA2ss2UNrKfm_4o+Eu4k< zt(}*3ZjER3VhsZi=$nmMJ7qspFp|?VfAzDuL zVG7gYAo*xTm;w~!uT^0RQ5}C>1b1q3*ZPecHwqf9c|q5q+mh0mhS|l3xs-J6kj<#s z*8V=5*SljM!<2o0JF44#S|?*}rM%vD^WYXm8VwUcibrtQ>PN4?Z1 z=$7lGchn4+ipFq>Eun5`wKk|3Q@7N-X{%{7Z)-+g)$$Wyb96Fvt5e;1q5wkAsJ5w& z82OB^?1o}PwpK){Siec34~OVxMpye> zo8+||=L?&&P7N5}!Y65hc6~5b_;{_zSDitPT4NXPn-;VJHN_a2sN}>xw_pj{QUfI) z>_h;3==$FH<}KX;8bv9QES8=w6%Bi$Yd3Nl(%=qbROfIo5MnU5L`yyme{ZUBrt62= zGGLm2W0Vcitptr%R%_RvFO+PE(6yXGCMSov$~$m znwB1>pX91CP9K4sjJyy|LKfQ|ru*opSjbO*SFTlMlI8 z>&(x>wzhe_e!|&v0i0d?42e^8NEi+rPb*}4S`Zbo&LX%>V{~+VuNXzC;HAiYih&rMHDw%by z`PO_2{Z&n#oHn73X~%VSSl9Eat>qB=NHpVyJ=WQp?=$lwMlq+_W15X0UENTS zqzsjE8`MJ4#728UMYvAzSxz>IyV<0F(_Ke4Q@Phr4GYm-H_x7f)S+fjX)1I27YZM87#%2AW1V%pV{&u4vXl>1!I-B2r=CoMY(RGtiaGJF*h3Hw%dWxR6xjqVt%;~ar=1V!f zDBTX_&eQYE|H2%3RV)hq9zqSTo!w?p-YW^gh0w;_6s{tn%xES)o}g1Xw0wM|#K%-p($`@BKl zV%L5fUa57ULjMT3jic;;!r=eRRqdbXJN)wz-i4wSl2GInkqy%q=^P{U`_)x+Z&e`u zD_#P9W(i@+O^V#92I${7qa%X69Q^_M4?zM!`Cl-?f{!|d-r@es91YX|a0LE0y^HEG zh-|`XDnQdv47PC_g)m;nfXcmM5vI`s9K|4C$HOG2L}6pW&A9LlzqsmdE0qc zFKcU`*O&>vP~dqHVD|%yzRm*rynv_!#&%St;(%C;X6Sw1qKa4wbTcdu6wwx4(l$?< zxnx+>i-wR`CK~4z+yz_rs)8$;U~;iS(E1_0h}ckzx?L*fk<_o>zkeSntANys{A*@( zm{Y8(Joenv6@dqTs?RnL3?{2g;w&bi+8S|jNURo@%-xn$1YV6xP{g<<=AGvrQt!O| zvulvlELuWhomh{YiWgUJ2~`2v*{Mgf{d2`A3&}y$h)cx=HW!|q4Zv!;lts&Sz|xDo zqmURDQ6L1%F(8Cz<8nG6;+14{flx(sL6oK2gJ?w1w(WC&t2drS3%1Sks)yJlHiyJU zaT%-v`Qv8s*nU(VvxFQe`om(2=ng`s9$X&hxJS=$c-y$u6qkzx%fQopiBv|*xEx_| zrL%NZrM&SSu16W3caLkFHfhlHdLNt~7TeJPieAyjeP4~Pu^LP}8BEv0a4KR;g>Z%p z-iU8;P_LbTIeExL@~x;pn-m1zhAZ0^OiyArUjgr{WuwvrHr$eQnpClmb=)X!nDh4q zblExw(-49e?*$HBXKH@kab|(B1L9yv>=%cy!LYb{E*47#bU0y=LQ==dO+Mm(%ZP9i z`i4;ih{ex&J%7QUvF1nh`W^a+R?6BHdf&Y5IR9pUag^PB%iO;!{a*zsVi@JQ(){7E zX_u_NFo}ASl5CCoT{bOV1iR2VIaU3WT1D=kdhHIl|Y1hCxN~V$`Iz@XY>673B zw7rj1vmLmAtq}D*L#ajdJhfoHC6!7>8xBv=5h#0#+G6tjb+L1FGb?x$^l&QqA}x)7 zJ?DLtf-%qLN%D%9s*lKAaKvIsLrNGf8;l4k!?X z!~NK}53^$sb{yXN7{oq|*-7xd0bsm;g+0^Y3zAMFu2*@Tq4U*_m&kjjVeBmB_nf0b zD&dVykyXEpz7$CKB3^dc9jR{r!_*Lu_&iPiGX2CP+)bZo@-KRX{r-A9;w{t3GJO>L z@5lZrdcf1|Yx2dPdyG2cO}@+OY5MN7^k6E1%@4ugbrJ8fjb-}OA&AG+rw^Tf^Z^lH z?_fEPr1o8%1yGw?u*ZWHcXxLS?nQ&U6)jfWic5h&(L&J_mtw`;rO-lfw*sZO6ewP_ zSYP12x%crhlgZ4^Z+Fi*^L?3on{)R6VYgOClQo5HdPC|5zEXHcl4#Pgf4=PUd_uL= z05!G4RczFp+a!clc&B+xg>wuFujYxXsxI|9hT_LbyLF!hb#cOxgi+o^B^n_Co7yy6 z(jP1a)bPV>o73*~IDFfy)v9JzAm;9US#Q!?BPqL~G*8NXF0jn%-TpM=X5|9S(xSkn z+-^VP#3Hif^QaB8E}w)INtb!51R(9NIAxlC9`VsD)1y{*H4Oci(1iHDS*K^~<5PUa zgWG<;H|gfjj8_A0mG%jKAI1!xatW~TGwOF>CQ7@Qn+l7s*(CLkP1cvrxEP$Z^4@Xv z@2F4|FrSt!_>y7*fz3z;^{EQC^?c$|@Wbmmy% zs7(sdcd}mJ@ZQOG8y{sOS87a8p*3`hiQHxT4)soFUP+1sj!O|RcldP{sIG`XCASkt zWm<;3?Hjh-O_a(gGE4R1oM$-uhj-aT4hv1)Ri|ExV1cJ_MX2%$fWnCpHLGs#RYg*E z;6#2Od81}%3{Hk+{8J<7p;#-_lNmO(1cS6|J_kA;HU zAzNPL#$HVj?8mx6`TM9Om>$uOGJhovF9Lk^NhBvp&0OFE7Y(_pwwa&>0Q1J>ceMG%3duGQp|?bP6GzT*7V@B+NZ@8A$6 z18q;%T}|j%IvSeLf~$k1&vNojaaT_Bn>oA-t1609skdIudc-X&*XldQ@fuk~?~#Ed zXX04m+;uGH{)C{Iiz%)6^t<<@vc+_uf&<9`9qk;?+B>>(%xj9d){hbfE@-7~#-hoG z*N?&W3(fg1pqfFSmBgIf*-#Bk>fzo*ljFQ`O<#~H}qrsL~6W4p~8Efb}BsKLsM3oLat7L1;nm+(AIJ_OHsu(PEb zmw7c?nX;x?T*?=#wG6J_tKhFKGxnQKvburS0~$ApS-J$8`*+#Ch-FJ2>pA((loN(qT60_48pE z`-PoIDMMkRoBmdHIB}QJvbE6fm4BlFGYmY${lPFwKOMRr46|L!^U%R;;7+wgR@i5d zOn~gN&d+BS6Lt0_ar&w(mgzdr`Uq>|3dm4%L4x)MYns%>$`u+L<^Eku09>V320r;1 z6aAh(5xf^#+4V!cD9-2m-qopd_@}eIS?7KB4i#Q?(_FfgVg+3Kvr}|FpbN3+_9Z2| z!$5I6v9e;?xelxar}w9#b2Rq!sY`FR2k7+2xot~fJD_q_y(KXf!9p}ImQYS56{$Y( za0_YeWBtD6ekh#8F?v>F;sO9;G>`ww3w;m(!wt!>esIU)X6usxH(cVEAX^R%^z_H>BN=8YFBaPC?%iQcR2e$Xfg| z@Lu~OR&Ua;%nWGYXcCo=Bj_dfa>0DA=g?7KlbX!@>H^yx+FXMPE&Q;6k_3UYVyhxI z=ZYbhy_Z`_Cndm2QNKeN*i!@(pCsi5dhxA#8ShlXAKuVSu;>tb43bpPf8TYJeKU=z~RPxWQ@Sp>{~%uSFSJFGHCQl zEQ-YmJrgu|0~65)=@^jKp>$V)q#G8MLlewza~2E~NRS?1o~?8z+LU6+PghYaUD@tv zsX&P+))C-)9~8|5Ys~=Gc^5Q~G(}6I7bUNP>L??UPaB>1eKiS@YhPn(jlB>BJ9m!c znuXo!Y>MlqUy4Exs`h8~=fcW~Whu3}20k>GoV3PPjZK4RMM($>yXd^ULe*)ZuLbf$ z@1t(U8AlSTCTIgF!~}4&SOzrX34s_>nTcw}Z<2ET(4c4Ec3jaU_=8`qZP~uJk+j&C z*o1TmGcE9-AEbG%(f7qA-Ubg_#i*GihhPkI4pWoR+EC3cj2LAOHl*bdd2E2zDBnpG z&uA3pO*edGVO5FDbdIFEwgYT%Mq2Cw#pdJ=x#N%Ivi;6-d^g))BsyE}e$ksiq9bly zydi(M*mxPxH;iF|P)3kk21=L!)IVo`|E9n?9w{S8+k)W)4fFNbKsy!3QLyNlUGS^P?J7uIvJS{#VAD#5H35gxAmN=f7ZX7Y-5eBk_-W6%C`q zy}8T$7(908u=gD-l!jJvjy%oPkT)Gd1av|zF*_m7MaFE>Lw)W%zu7rj)o*0wOz~Oz z@?1zW1hXXY@!T|hbm=HPtW%}K<8fg7G&OKvE{Tjxg|mIwOQ%FMK`BN9)sEG{O$O4m zk@p@Ubu(2LUDT7$cdX2A2o~z}Z&ovp?w^4}x%&bmRN#9)89MTMTx|VFJw3R)S>ZN= zYl-rTV8*86unCGHY-wT|v2>y$T!oX`G%n{X4ut@RJK~WGIW-uj= zQ>j(VA#DeyC=vGh?-%2cgl5zSDBw4H$^v^hi?g`IKHEi|ML?a6g?Gi`K-?O{RSoHD zOstehlo(6p0olcvE-BOK;d*&~Xrf?J_2lr>AD$9g&c3`^F}4VfOUf!~gNfM%N4xU= zaX%m!i8dYZ$$2_HK2r|y@ryAuZ@CC9C~S8euhb1Aq!UX8Uq}ndD(X7BLU2gc`>>JU zWeGU14Ex2c>LGz$2kj!! zFickTd7?ZpC?k4fFl@=>)$T(M#G(d)vKamRPn?rdM9z0wP!d_9EP4_QZU%)bL4^?Zgec^OeyZ&&*o9)fcz8LTS-yu%j4v%$ZC71%Xh87Kr{R%3Ra|*L)Nz?Dun$D3CC!i zR>D6tIj)O}Uw|N17Oo~4{(jSQvH6RX{HU_iaaJOevC+T+cR@wU#>;J>Q9f=Pnar-) zAuz2n8VxSn1$4*?0qTJNPX0wO)I4znGRW!O<3jN`>8y8l3zH^Wk4hg&1;<2o|#XZ2ukL>ycB@tCZxXmb8>%nIkpS*M;zxxy5 zjqd7V1(X!Z7-4S0sa#tkTVCl?yuTnC@ZCq^;vHc$TcwZaPs;_zmt*|-L*{a(`VDx~ za&H^m*$x#L*EwIhewT z&T_W{rVZxo4pG+JhgaN*7YY^TfXP+LUK}dzU$P`vW7sDi$1b4?Q{+1sbM{D$>?B$f z)e|Ed+$Plp2&#ENkUE&%t3f3&O_YxX$;9D@qLk>{<6RMTrdO)83&&BN_I8R35f|Xc zW&%K)@t=_8te4T9OD(v2qMl)?Vg_1H=g`a-^9{$~_tcPr> zAAkej_4%bx^nPnTFFemu!gQUq3Y*GD@&Mi(_os2Pc@~z>tB%FRFeqM=d&+5k`dY z&;y-6XxU~%n|pfj_m|3}V?7ea-ASW3`NP-;-101=_m$56G!p_s+%*~5#$Ae106pWp6B-@GARIlSOK?cJWMt%Vac zq}=#D2;#G?VgGi3>`B-Q9%MD{lh?Opf^M$`8ciq1C@%KxA}?Hx5qFx$+k@#&$#7pv zZpJTOAdOyxP}Zip(OpRO4D7{SSdQ0p<@*V&#~dBY#?pGeHoZygLkHU2E3C&*pYV68 z1vt~Jx87cLpCFy2=Lw^0z+^99&Q(|p_nQrTuAK88X4Bh5q365n>@~kaGy=U@x*d%zjSyQ()Bw9ra2AD*8TRFvnATpY>wlWhho?NqJWhTUeiHz)-o3 z9?fPPT?Otv^^chT+@;5rnMD+7&6h*Vj%3#L7@~yV4fIVleAQAc;_zbMgO=jO| zs3jsRC*=Mvi`G^*hlFoaCWQQ*>0yh#qy{nPQUZ9@I;~lQDjC15VhhhW^5Ud{G4Gu! zAx1VoXLu%t(1oZdNJR@D0dl%W@>93Lq}anm4WxmR&Yv;WmZIm5;oQO3KYg%6fEg(Z zXH;z$?ZpkPn>BiKzG3%caMj-V2kBRekyBZjgoL?WweA4P3|tJFU~-#0T%l*HP>z#E z8UR?*CZ-yM5%NozVNq53_g%DodfZ+Yz@@7)h(kI}Skp^H2bDV*<>&R_&;f=JiOHC_ z6i{}>q6A~K(z%218j@0S+y+QlSBEo@52k2-_A1mdW%%ebZ|;a9z&Q%7{{X{OPehD@ zHKP|(O@BCDEGIcHUoq1Oe75KkM5Cuf$9|OrE1?2}W zC1N{;6uM|i4qS_s%Ur)03D$D8U&o}lTagvo*E?ME5dZZhTxN7VVp!gi^FEP<`1*)$ zte473PaSU0rnx-mvYK!kjo}`kf@vZxU=}z_gHv?!IDK8zFV867>5Xj{qN(&?NH-G4rC>uj@ct zBQbrbyD8sBD@|`tfy8pjUu!f>U6U2w+ik-l+v z{mgt_TViVEb*7Ea(ac`{y|h2p_>CI|H&E^`c+Z(y@QlYV>N@(z*Z3PZ0&bo~-6aqI z2AN4Zj?`1Um!)S3?keti)z@zD)mkqqeN+^ELkEa@CZ1P)E$0x^U+(@9^!c67kXMOb z;xU!1mC?7}lshRC!J`dmMiN-9h<=U!S5W$b`^_;bH2d6N@hK|{vqAyfz>X~V)%-?KHR z4KBN@Ilp8@3y!T^!gFx5hw?W?52dLOQYatR}#@;3ZxZ`;r5fh}kig)nN#ouF6Ewf?AaE_U{Ddv~f zuK(`fH?t9JH)y(a0#>~ow={O(Bzj2a+x5WJ_h;U@TJX3rt!H3Tb6k$MsWyKd;+qt# zCTE0YQYW&M&*KZW;9bCN!QsR;^L@_L>%+eMIT`o#CQk4^^L7XIxCP_Mf}*mf3>7g4 zjcy-f4=0$CsMwT@Wda#6doKK)7@YSpB$U?=r`B^O@EJa-XwUXNC-)=QSg3J&|6SOV zOz6_Id-B62Z=vp&VhK`zrspBVRvW&5hD4M(-^N}MMNY<6jtK{Y`?KA!<+HSUrESH- zHpZ^-?qU+9#XlNPypHX8h8l|}p`R88{c8?aOTu~zisfm{Skxy3%~s!H6!9r=hOC|ShnFP4mPD34EOxBot;zk7m}{G%C&hD@~g zbI-V8B0DF?@)QdmSpD2zrZ{QYj`z%3%vvI@x*Dgh%WVVBF$Cw1U-~(Smw!qpZU{gWOQ(G{0WvGw)GRsrOl1{nXT-?l zd0l;@Rn(XCp)ZRI`{nZgCK!zQqk@Jg^vPELJjx5404-bdAiTw;h^yDCa*&ncS4b8W z;RkUL#S#O`SruC4hezTjn7jlZ0QEsu=YL<}_y9;Az62zp1P2Kh2$9ofXt{_Cn=K9BKSRq9DuX-v> zN}B13Zv@W+MGN@~D=a+B=RYaL|4)uVP%34R9+mtc8kL0bRbmj-N;=4!5=O)a5i3Y- zB@u$91OO5s@won!|4Adk`b0m;c_{Nhk-}3jo=^xN0E7xei~fJKlprA` z)Rg}zPXGYVpLobCKX@oU%!A@V03jZ>T2!#r;(kIUt3tYJ2q6O10*H@2-Ce4Q;G@+a zZ9z3C5U>*WV}So!Tmu07PXefE{|l4X@KXHWetfh~z)rpY1(_)xnzwz24W|w^9L^_@ zjRg!+r1-aK84P;54h2>)fE+?&ijDy5_6DITp{MxoU=RSn_9PmD^&>1*%ZB*4m(Hb@ z2>xf_<1jL7StuU1d^x}}Y{TA9hk+3D2oZAD*;$VUcWLcPH1AXg2weU~n44Bl!5M w7PgL>&;I`;?us74{(3&7f4)He))T^OmOUET88lJokpk8iEZ0%Y};;}6WeLhG)ceS&-?w@`}e-CJ+s!V zSu^$txpxNH=!yz#X{~D|!Rqmyk#lN~X&X|PN-VC(*S{l4u_MT_%(!w!9}$Uk0n6R( zL%pgVFwqG>LG8`I2L|;5AqL>R@p~M3nvbIPkUGU1UNfg*ZauQPW^~muS7cbUWCOm& zP{?3pP$V2-95b*Q&5a|Od0agz$|zeVRaw(<6XfTiP7(r>_dccUn9+Z$OIAQHIvk>h z-e>>h2IYFA7XUz^RO%fk^G>D!fyWjAhD)3jY%kZDE?g1Q*iyD{vH)#Q_EPC(p+ZDo z$G6l>EuZ$n!Tme2S}Diy_50eVaJN?#7YR``jmssIO%c}!MG@dX0H^-IbZ zDWa4@c9N7UL2RIv#+LfBDwa`1TUZ--NgTb$yk{XDga}iYdLMp2q=-Jw!N%7|lq^9Y zo1&b|ArSu_@%g>sA`&2Q_>u}xtYqFt#F9;%Ym}2ZQt90lzAoLHBd92%Vhg~=HI%8;6Z;I*t=r~oAQ^(Ce z$tFjU=&C6`fx|6I?f?taoq*2Q@%?a>ww_4Q%-Uj_?EQkM+vm_GPu8pe6lveXxY+Y^ zlaZ3m!a!*IQDy5e_qY)(ho2=cabN$zhJa)pbf5?==6-{4l`i3tma zC?Z6zT;g(>&7Vg8BP}62RGt^}eAThhTWwBoT}c6txFgn+IJoQ(LR26eSprJFghhedF~s`k_<91{q-vf($kZLm;kO5CyrANi2N;X+hK}}o z@as!OVxzbXf$(^yWI|Y`sN^Ir zB?!|V2r1K0hJ}7-BqX-ERHDXKwUS8|6(t8X#!w!>zSVv0=Gwk~WmGdVfqKvTDu$UV zi3$8JI>i@APR3=|qu_0AlW$|~?fvVefV3Z?mSX(w{O-=`K2+^+tuL{y$y(Qo(nU9T z&sCVDGnngR0LM~i2vZ2_X#2Rx?i$fS)bXtd*ra`GO!pu?%pSPQw&M-hGB)~l=NjHv z?K{-KE1UoTv+&+7Ys-$OiPPx_SUMqKtF!#T&Cp4YDQDIn8)s*O?Zx0qqt5TlH)Vr5 z#v&SZQo&-HXLf|{n=dmemu2lh49_-ckP&y{-VBc*0O3DC(Hp5n`BHJka!}Q3z=x^< zMQ{tD+ULXQ*<(e#%Ls+dbSD5Hn|6E<=X~>)+?njf0$ctF-ho@JX$blCV`z4vyXJ~8 z+^}bP&$vO)zS}t#gIc#u*%ivLFWKM0>!-nIvwYTjbA_%i^IV|0S6i~n*6oZz!EeE} zX4zs-HBP1tuo-@B6V3`YUNifMI~HU>UZ`(NITas%uRt*V2`qLk7*;~QCjrYuM|l~S z1M&Q`t12hy5_>J}0M3dxR$gv<=$g;jJl?Dt^=s%L+F@IuGen!c|4_6oL~`bMNPKsP z3{U~F7nSYof-?>~AW8A4y2-+_G`)}f z1N+(~`BOnyHQJJp3+vDJqY~JCzFii6h-!+s#~}p#b!7&& zX&rh-Hq%+v(=yR%uD;Pl9zN;=|I9wt1j2yp*=&rKOkiZ1qaSKXdzZP6@c+|V3SWUFj zbA~b-Z08Y|k=kf$IU;60v1;)3x`0jkyh||%oLyWCw;JbSB zl72>_X$Ofz_Y(ZmoReF7cxu265&|)RIB4e%)WBA$;N~1X(r5M)1WW<%>I!|3QHIs{ z`;Ojg!Q`D?4B4n+P4H1m4B3I4$IIc3M5A-eoE*>T_m22ewptA*QPmBEVq?caEAk`x zM2R%lVcLr<7;pIMv@tf^3!c)zF$h@vWVbC@zVU^_F#m6lqEgCuuoUA;du$#rojSk) zLMa&ByKlI2he)74iD`^JOWOv70y9sSLdLWT@fUif2r`(Aq*ONqiSaS~W3eF}ENosS zo6DwNGeHCIFn@rf(jdHa=!80evnguFroVx69AEjI^tVjQDcD&(QLGIZBOq&mCZk2S zCJ_L2?UYy9$B%Ef@Ov^~(|fLto7wD-Ptb}KV|WUn7jBx&EZVFxEyYry*E%`Was_*G zB{C!XY~)GgaHo%H;l32z%&q#*4EjUAXdkvd6HGJRROTQuXppi;ve;#6;xH#AHh)wF z%Qqe@^$ytZd5ULL8TkV&knbV0AZf?PK;pWy6Ijgd6N9@(Z-8#@re!mB*2e~e;NNWX z<(>%4djki3OMp(M|L#9VegX;JZ;d=8l zCvX*q zDD9}o*=x1@x#$CdQ;{`YhT`ABub?5TqiTH1Y088SNJa_&E0*V1NMXQgA0%kuFWR$J zWH(EvJQL+X)`C2=Tq^I8_&FiABP94Q zg?%_$KWR%~Tg|-+V#zK>Vf)WN|dg1*vZR zruO6PApbS-j0Ley-b$GrdREN}OQG*@p$s49m0a_tqzxv>$8%;KAe zFp)wNc!nz{AG0}th**_<$wcuFInbJ*0*;33gL*Qq`HD}Dj0hJ36reY-d}tZpN0}a^ znx#cdZDGLKH3sCiTKbVD{vw6ERQDQJLnM4fq9hjf=h142pDhXN&?>cv25>F4~^fGjosOWWFOpM zRyS`dHg!A$EoHM-cj%=Km6z6p?b~JQ2Q>iQ>!4228bX40>HwO^il&G$=O{7>iOwEj zBrVTX=(OPM$pxoLH0G^eK5WtqT&`v=$*>w(_DNrNv=8-1Ypi8FHr_hF+ZBzFJ?3Oj zVkojNPXG~+vYwZ%ciz)XIUOGbALw%jjy(Z6P9`aoU=K|*p8m{LCzAFVK4A&V05RLYLVdC_ z_&I`_Hhma2z5bMj1HU=?D7ODFJgbqDarCj+G6Q{+%%-d1n-qNYQX|Ws@l#96PcXuv z(#_Bq+&-d6-f8-@B3$;jxKfBe_*o9I*)0k0ck~Sh`wpIdQGLW*!U1SOKR`9hnnrf$ zGFitw{g>>&D683bj@vHuQ}>KU2_9>685SlFb?z;`9CO=)o=-7?m_0&3EB)4#^;ngKmEq&zYmHag;poGfezAyQxDr<@NE} z`Q(oN~pfBiw+6ZiBaIRuEhXf!WycIyB{pQ(^9C0i}&J3ac0rR(u^OFqE$B5RZ~b=p{LF3&V(FlYCKdBV+}3 zvxgn25^#6N({f%h0x>1biUXOmhZWlI@(^SF@%q+thF{LxUp5A`TCjm%dEW1oG);y- zIXZJ#Pv(5Uv`uRUuQ05dHF(PAzt+Wmul^1!COkn~g+ zeN6(URji*!bk}krVu$QbR(R%`!5xQNpZE^HO7Cw1tr3H;1A%926u~={s}VTO2vT!i z5oyX*D@;LI*3jnsI{EpcslSl_wP6-*2{KbS2nYdG2nbaLs1#T!+?026bs$t%hO(&u z`rZQKW|Nl&sO$S8-Qo!JVC3LLd-ty;t<9~nYuVT&(gT;fP#OVD(O0NmJq~)-v-aty;i#BD*gp9785RSy#rV(L^^ES_fXwtaNF)u!=8C# z)wkaa^&Lu|a^Z^LPxATAWRnSZ_?{zzd-3mO>F}&7i6@2G=q+;1wt*^|c=Tf+VIV7X!8c|X05xc89 z)|(5dg|H-$1V+EcGg}&#(h>=&fpgEb`VBj!09{n$DP4VmNleQ;QJp#O+~UNdSf|7* zmr0?e3Xk3wgvDtYgJi$rsU~zr zmqIjT#^pbVtsIjbDgGO;GX8J8>ZURTiWzK{8I~D_X@-EH6`&+TfD@iRx;WnLmfkUF zl&A-suM)@^l9;3e5ghqWVx81GO4dGok9oI-Co{LAqCsEq#)XDY4-Z#oWLgKFg~0?D zE!8eH^jfR}f6`|IYtHPI7tz8L%#dynIBr~3mVLtdPSc1~@^(+!Xw@(Js`vwdCe4t@ z!+4~Gd3codGlp+28ICy+E)fotE!g#To#L|7+z7&GOC`EtHlQ&O$3LrQMFrUukko1} zcX7~ag#?-`=2|X40x>UjIhEl?#}6A>WTieB`icK)xPs%i5q14HMgQK$MYP9P*UD@7 zw!+DE@s}QO@qir6vC~1pIjjo2z121Tdq;d_0EYZkd#wLSG@Rq><@bCS<)bFKfG16S z!@e?>0ZA5}4v#fbZ2Ogut(Con@4b?&QuSPiU~B>1WcL_O$jM_}vElb%=M2>@C*A!w z)*@tTLf3+KXLT%3%u%q&Y$)z1l&ADUsIk4#;w<)#!o7}9luq62#Wz)8^IP?5Ltz3q zpYMr!UcUJVe*LAgQT{C1W#hay_1$*k;XR8^QwaGG;SGQDhK$a44DA5qR>H{`ZdCMV zC5!GrR`QN0w4JkCD=GvlTyL_0weG0ReWN|b;P=(r+kt(2(I0s~^~{6Bi-$mRBY8LY zb2cu3i9-N26if*Kx%>`@>v*G<=5#-j#xzZa5NkmZ!ro)LP|YLQt)#{XcS1kG2H4J? z?51UjK8G*=N~_uZRVS~ApY2#)S!}|~xDjTjS0MXqA#9T=d~muhTSZvdz(jx4T1LyI z6f?Oc$@aElelfpi{MrirWO1C7&;Z8tVChzP*nzY1auPO^YLy?C&~tySz|tc zVK#lPBx)_|n>#LQ_0Ih(s2=oDY?L>^GBhnFtYRDn8t>T61(T8Xc7rV^(V?an8xp)w zd(m01$h~k$*zT(MP~Asab2NE$&t)nw!$s1vNldkZ!lCmksw^%XB{xb))>*vCeoYB-`MJ{F;9c7_&Rr;o7KQOPy^=MNbH>&9i< zU%QTnT2RE<~unJT#U+vWgSJVjEk2Ly+F&^<5opYJQ6I@uuPC&6qmTUWmE4 z*9~JRrYk9<=kzBx!E~J#-U0smu!1y~Uq$~6@W5m#;*


Xdyd*p!l1Y@nCA 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6..c61a118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685..f640dbc 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob//platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. From 681f977f8942a1747d7b34df51af23a247603cd6 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:00:28 +0800 Subject: [PATCH 46/55] better update checker --- build.gradle | 2 +- .../milkteamc/autotreechop/AutoTreeChop.java | 11 +- .../autotreechop/ModrinthUpdateChecker.java | 480 ------------------ .../events/PlayerJoinListener.java | 6 + .../updater/ModrinthUpdateChecker.java | 402 +++++++++++++++ 5 files changed, 417 insertions(+), 484 deletions(-) delete mode 100644 src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java create mode 100644 src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java diff --git a/build.gradle b/build.gradle index b46a173..60b2c17 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'org.milkteamc' -version = '1.7.4' +version = '1.7.2' // Java Configuration java { diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index b9849d3..b965e39 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -45,6 +45,7 @@ import org.milkteamc.autotreechop.hooks.WorldGuardHook; import org.milkteamc.autotreechop.tasks.PlayerDataSaveTask; 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; @@ -64,6 +65,7 @@ public class AutoTreeChop extends JavaPlugin { private Metrics metrics; private TranslationManager translationManager; private ConfirmationManager confirmationManager; + private ModrinthUpdateChecker updateChecker; private boolean worldGuardEnabled = false; private boolean residenceEnabled = false; @@ -128,14 +130,13 @@ public void onEnable() { getLogger().info("PlaceholderAPI expansion for AutoTreeChop has been registered."); } - new ModrinthUpdateChecker(this, "autotreechop", "paper") - .checkEveryXHours(24) + 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") - .checkNow(); + .startPeriodicCheck(); databaseManager = new DatabaseManager( this, @@ -331,6 +332,10 @@ public AutoTreeChopAPI getAutoTreeChopAPI() { return autoTreeChopAPI; } + public ModrinthUpdateChecker getUpdateChecker() { + return updateChecker; + } + public CooldownManager getCooldownManager() { return cooldownManager; } diff --git a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java deleted file mode 100644 index 0e10e19..0000000 --- a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java +++ /dev/null @@ -1,480 +0,0 @@ -/* - * 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; - -import com.github.Anon8281.universalScheduler.UniversalScheduler; -import com.google.gson.*; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.logging.Level; -import java.util.stream.Stream; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.ComponentBuilder; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.TextComponent; -import org.bukkit.ChatColor; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class ModrinthUpdateChecker implements Listener { - - private static final String API_URL = "https://api.modrinth.com/v2/project/{id}/version"; - - private final JavaPlugin plugin; - private final String projectId; - private final String currentVersion; - private final String loader; - - @Nullable - private String minecraftVersion; - - @Nullable - private String latestVersion; - - @Nullable - private String downloadLink; - - @Nullable - private String changelogLink; - - @Nullable - private String donationLink; - - @Nullable - private String supportLink; - - private boolean notifyOps = false; - private String notifyPermission = null; - private int checkIntervalHours = 24; - private long lastCheckTime = 0; - private UpdateCheckResult lastResult = UpdateCheckResult.UNKNOWN; - private boolean suppressUpToDateMessage = false; - private boolean coloredConsole = true; - - public enum UpdateCheckResult { - RUNNING_LATEST_VERSION, - NEW_VERSION_AVAILABLE, - UNKNOWN - } - - /** - * @param plugin the plugin instance - * @param projectId the Modrinth project ID (slug or ID) - * @param loader the mod loader (e.g., "bukkit", "spigot", "paper") - */ - public ModrinthUpdateChecker(@NotNull JavaPlugin plugin, @NotNull String projectId, @NotNull String loader) { - this.plugin = plugin; - this.projectId = projectId; - this.currentVersion = plugin.getDescription().getVersion(); - this.loader = loader; - this.minecraftVersion = plugin.getServer().getBukkitVersion().split("-")[0]; - } - - /** - * Set the Minecraft version filter (null for any version) - */ - public ModrinthUpdateChecker setMinecraftVersion(@Nullable String version) { - this.minecraftVersion = version; - return this; - } - - /** - * Set the download link - */ - public ModrinthUpdateChecker setDownloadLink(@NotNull String link) { - this.downloadLink = link; - return this; - } - - /** - * Set the changelog link - */ - public ModrinthUpdateChecker setChangelogLink(@NotNull String link) { - this.changelogLink = link; - return this; - } - - /** - * Set the donation link - */ - public ModrinthUpdateChecker setDonationLink(@NotNull String link) { - this.donationLink = link; - return this; - } - - /** - * Set the support link - */ - public ModrinthUpdateChecker setSupportLink(@NotNull String link) { - this.supportLink = link; - return this; - } - - /** - * Notify ops when they join - */ - public ModrinthUpdateChecker setNotifyOpsOnJoin(boolean notify) { - this.notifyOps = notify; - return this; - } - - /** - * Notify players with permission when they join - */ - public ModrinthUpdateChecker setNotifyByPermissionOnJoin(@NotNull String permission) { - this.notifyPermission = permission; - return this; - } - - /** - * Set the check interval in hours - */ - public ModrinthUpdateChecker checkEveryXHours(int hours) { - this.checkIntervalHours = hours; - return this; - } - - /** - * Suppress the "up to date" message in console - */ - public ModrinthUpdateChecker setSuppressUpToDateMessage(boolean suppress) { - this.suppressUpToDateMessage = suppress; - return this; - } - - /** - * Enable/disable colored console output - */ - public ModrinthUpdateChecker setColoredConsoleOutput(boolean colored) { - this.coloredConsole = colored; - return this; - } - - /** - * Check for updates now - */ - public ModrinthUpdateChecker checkNow() { - if (notifyOps || notifyPermission != null) { - plugin.getServer().getPluginManager().registerEvents(this, plugin); - } - - performCheck(); - return this; - } - - /** - * Start periodic checks - */ - public ModrinthUpdateChecker startPeriodicCheck() { - checkNow(); - - long intervalTicks = checkIntervalHours * 60 * 60 * 20L; // hours to ticks - UniversalScheduler.getScheduler(plugin) - .runTaskTimerAsynchronously(this::performCheck, intervalTicks, intervalTicks); - - return this; - } - - private void performCheck() { - try { - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(API_URL.replace("{id}", projectId))) - .header("User-Agent", "Java-HttpClient " + plugin.getName() + "/" + currentVersion) - .GET() - .build(); - - client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenAcceptAsync(response -> { - if (response.statusCode() != 200) { - lastResult = UpdateCheckResult.UNKNOWN; - plugin.getLogger() - .warning("Failed to check for updates (HTTP " + response.statusCode() + ")"); - return; - } - - try { - JsonArray versionsArray = - JsonParser.parseString(response.body()).getAsJsonArray(); - String latest = getLatestVersion(versionsArray); - - if (latest == null) { - lastResult = UpdateCheckResult.UNKNOWN; - return; - } - - latestVersion = latest; - lastCheckTime = System.currentTimeMillis(); - - String currentRaw = getRawVersion(currentVersion); - String latestRaw = getRawVersion(latest); - - if (compareVersions(latestRaw, currentRaw) > 0) { - lastResult = UpdateCheckResult.NEW_VERSION_AVAILABLE; - } else { - lastResult = UpdateCheckResult.RUNNING_LATEST_VERSION; - } - - UniversalScheduler.getScheduler(plugin).runTask(this::printCheckResultToConsole); - - } catch (Exception e) { - lastResult = UpdateCheckResult.UNKNOWN; - plugin.getLogger().log(Level.WARNING, "Error parsing update check response", e); - } - }) - .exceptionally(throwable -> { - lastResult = UpdateCheckResult.UNKNOWN; - return null; - }); - } catch (Exception e) { - lastResult = UpdateCheckResult.UNKNOWN; - } - } - - @Nullable - private String getLatestVersion(JsonArray versions) { - return versions.asList().stream() - .map(JsonElement::getAsJsonObject) - .filter(version -> - "release".equalsIgnoreCase(version.get("version_type").getAsString())) - .filter(this::isVersionCompatible) - .map(version -> version.get("version_number").getAsString()) - .map(ModrinthUpdateChecker::getRawVersion) - .max(ModrinthUpdateChecker::compareVersions) - .orElse(null); - } - - private boolean isVersionCompatible(JsonObject version) { - JsonArray versions = version.get("game_versions").getAsJsonArray(); - JsonArray loaders = version.get("loaders").getAsJsonArray(); - return (minecraftVersion == null || versions.contains(new JsonPrimitive(minecraftVersion))) - && loaders.contains(new JsonPrimitive(loader)); - } - - private static String getRawVersion(String version) { - if (version.isEmpty()) return version; - version = version.replaceAll("^\\D+", ""); - String[] split = version.split("\\+"); - return split[0]; - } - - /** - * Compare two version strings - * Returns: positive if v1 > v2, negative if v1 < v2, 0 if equal - */ - private static int compareVersions(String v1, String v2) { - String[] parts1 = v1.split("\\."); - String[] parts2 = v2.split("\\."); - - int length = Math.max(parts1.length, parts2.length); - for (int i = 0; i < length; i++) { - int p1 = i < parts1.length ? parseVersionPart(parts1[i]) : 0; - int p2 = i < parts2.length ? parseVersionPart(parts2[i]) : 0; - - if (p1 != p2) { - return Integer.compare(p1, p2); - } - } - - if (v1.matches(".*(?i)(snapshot|beta|dev|rc).*") && !v2.matches(".*(?i)(snapshot|beta|dev|rc).*")) { - return -1; - } - if (!v1.matches(".*(?i)(snapshot|beta|dev|rc).*") && v2.matches(".*(?i)(snapshot|beta|dev|rc).*")) { - return 1; - } - - return 0; - } - - private static int parseVersionPart(String part) { - try { - return Integer.parseInt(part.replaceAll("[^0-9]", "")); - } catch (NumberFormatException e) { - return 0; - } - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - - if (lastResult != UpdateCheckResult.NEW_VERSION_AVAILABLE) { - return; - } - - boolean shouldNotify = false; - - if (notifyOps && player.isOp()) { - shouldNotify = true; - } - - if (notifyPermission != null && player.hasPermission(notifyPermission)) { - shouldNotify = true; - } - - if (shouldNotify) { - UniversalScheduler.getScheduler(plugin) - .runTaskLater(() -> printCheckResultToPlayer(player, false), 40L); // 2s - } - } - - private void printCheckResultToConsole() { - if (lastResult == UpdateCheckResult.UNKNOWN) { - plugin.getLogger().warning("Could not check for updates."); - return; - } - - if (lastResult == UpdateCheckResult.RUNNING_LATEST_VERSION) { - if (suppressUpToDateMessage) return; - plugin.getLogger().info(String.format("You are using the latest version of %s.", plugin.getName())); - return; - } - - List lines = new ArrayList<>(); - lines.add(String.format("There is a new version of %s available!", plugin.getName())); - lines.add(" "); - lines.add(String.format("Your version: %s%s", coloredConsole ? ChatColor.RED : "", currentVersion)); - lines.add(String.format("Latest version: %s%s", coloredConsole ? ChatColor.GREEN : "", latestVersion)); - - if (downloadLink != null) { - lines.add(" "); - lines.add("Please update to the newest version."); - lines.add(" "); - lines.add("Download:"); - lines.add(" " + downloadLink); - } - - if (supportLink != null) { - lines.add(" "); - lines.add("Support:"); - lines.add(" " + supportLink); - } - - if (donationLink != null) { - lines.add(" "); - lines.add("Donate:"); - lines.add(" " + donationLink); - } - - printNiceBoxToConsole(lines); - } - - private void printCheckResultToPlayer(Player player, boolean showMessageWhenLatestVersion) { - if (lastResult == UpdateCheckResult.NEW_VERSION_AVAILABLE) { - player.sendMessage(ChatColor.GRAY + "There is a new version of " + ChatColor.GOLD + plugin.getName() - + ChatColor.GRAY + " available."); - sendLinks(player); - player.sendMessage(ChatColor.DARK_GRAY + "Latest version: " + ChatColor.GREEN + latestVersion - + ChatColor.DARK_GRAY + " | Your version: " + ChatColor.RED + currentVersion); - player.sendMessage(""); - } else if (lastResult == UpdateCheckResult.UNKNOWN) { - player.sendMessage(ChatColor.GOLD + plugin.getName() + ChatColor.RED + " could not check for updates."); - } else { - if (showMessageWhenLatestVersion) { - player.sendMessage( - ChatColor.GREEN + "You are running the latest version of " + ChatColor.GOLD + plugin.getName()); - } - } - } - - private void printNiceBoxToConsole(List lines) { - int longestLine = 0; - for (String line : lines) { - longestLine = Math.max(line.length(), longestLine); - } - longestLine = Math.min(longestLine + 4, 120); - - StringBuilder dash = new StringBuilder(); - Stream.generate(() -> "*").limit(longestLine).forEach(dash::append); - - plugin.getLogger().log(Level.WARNING, dash.toString()); - for (String line : lines) { - plugin.getLogger().log(Level.WARNING, "* " + line); - } - plugin.getLogger().log(Level.WARNING, dash.toString()); - } - - private void sendLinks(@NotNull Player player) { - List links = new ArrayList<>(); - - if (downloadLink != null) { - links.add(createLink("Download", downloadLink)); - } - if (donationLink != null) { - links.add(createLink("Donate", donationLink)); - } - if (changelogLink != null) { - links.add(createLink("Changelog", changelogLink)); - } - if (supportLink != null) { - links.add(createLink("Support", supportLink)); - } - - if (links.isEmpty()) return; - - TextComponent placeholder = new TextComponent(" | "); - placeholder.setColor(net.md_5.bungee.api.ChatColor.GRAY); - - TextComponent text = new TextComponent(""); - Iterator iterator = links.iterator(); - while (iterator.hasNext()) { - text.addExtra(iterator.next()); - if (iterator.hasNext()) { - text.addExtra(placeholder); - } - } - - player.spigot().sendMessage(text); - } - - @NotNull - private static TextComponent createLink(@NotNull String text, @NotNull String link) { - ComponentBuilder lore = - new ComponentBuilder("Link: ").bold(true).append(link).bold(false); - - TextComponent component = new TextComponent(text); - component.setBold(true); - component.setColor(net.md_5.bungee.api.ChatColor.GOLD); - component.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, link)); - component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, lore.create())); - return component; - } - - public UpdateCheckResult getLastResult() { - return lastResult; - } - - @Nullable - public String getLatestVersion() { - return latestVersion; - } - - public String getCurrentVersion() { - return currentVersion; - } -} diff --git a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java index 8ccb67c..ce59e01 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/PlayerJoinListener.java @@ -17,6 +17,7 @@ package org.milkteamc.autotreechop.events; +import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.util.UUID; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -25,6 +26,7 @@ import org.milkteamc.autotreechop.AutoTreeChop; import org.milkteamc.autotreechop.PlayerConfig; import org.milkteamc.autotreechop.database.DatabaseManager; +import org.milkteamc.autotreechop.updater.ModrinthUpdateChecker; public class PlayerJoinListener implements Listener { @@ -61,5 +63,9 @@ public void onPlayerJoin(PlayerJoinEvent event) { // Default is disabled, so no markRejoin needed here. return null; }); + ModrinthUpdateChecker checker = plugin.getUpdateChecker(); + if (checker != null && checker.shouldNotifyPlayer(player)) { + UniversalScheduler.getScheduler(plugin).runTaskLater(() -> checker.notifyPlayer(player), 40L); // 2s delay + } } } diff --git a/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java new file mode 100644 index 0000000..eac8a81 --- /dev/null +++ b/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java @@ -0,0 +1,402 @@ +/* + * 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.updater; + +import com.github.Anon8281.universalScheduler.UniversalScheduler; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.milkteamc.autotreechop.AutoTreeChop; + +public class ModrinthUpdateChecker { + + private static final String API_URL = "https://api.modrinth.com/v2/project/{id}/version"; + + private final AutoTreeChop plugin; + private final String projectId; + private final String currentVersion; + private final String loader; + + @Nullable + private String minecraftVersion; + + @Nullable + private String latestVersion; + + @Nullable + private String downloadLink; + + @Nullable + private String changelogLink; + + @Nullable + private String donationLink; + + @Nullable + private String supportLink; + + private boolean notifyOps = false; + + @Nullable + private String notifyPermission = null; + + private int checkIntervalHours = 6; + private boolean suppressUpToDateMessage = false; + private volatile UpdateCheckResult lastResult = UpdateCheckResult.UNKNOWN; + + public enum UpdateCheckResult { + RUNNING_LATEST_VERSION, + NEW_VERSION_AVAILABLE, + UNKNOWN + } + + /** + * @param plugin the plugin instance + * @param projectId the Modrinth project ID (slug or ID) + * @param loader the mod loader (e.g. "paper", "spigot") + */ + public ModrinthUpdateChecker(@NotNull AutoTreeChop plugin, @NotNull String projectId, @NotNull String loader) { + this.plugin = plugin; + this.projectId = projectId; + this.currentVersion = plugin.getDescription().getVersion(); + this.loader = loader; + this.minecraftVersion = plugin.getServer().getBukkitVersion().split("-")[0]; + } + + public ModrinthUpdateChecker setMinecraftVersion(@Nullable String version) { + this.minecraftVersion = version; + return this; + } + + public ModrinthUpdateChecker setDownloadLink(@NotNull String link) { + this.downloadLink = link; + return this; + } + + public ModrinthUpdateChecker setChangelogLink(@NotNull String link) { + this.changelogLink = link; + return this; + } + + public ModrinthUpdateChecker setDonationLink(@NotNull String link) { + this.donationLink = link; + return this; + } + + public ModrinthUpdateChecker setSupportLink(@NotNull String link) { + this.supportLink = link; + return this; + } + + public ModrinthUpdateChecker setNotifyOpsOnJoin(boolean notify) { + this.notifyOps = notify; + return this; + } + + public ModrinthUpdateChecker setNotifyByPermissionOnJoin(@NotNull String permission) { + this.notifyPermission = permission; + return this; + } + + public ModrinthUpdateChecker checkEveryXHours(int hours) { + this.checkIntervalHours = hours; + return this; + } + + public ModrinthUpdateChecker setSuppressUpToDateMessage(boolean suppress) { + this.suppressUpToDateMessage = suppress; + return this; + } + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + /** Run a single immediate check. */ + public ModrinthUpdateChecker checkNow() { + performCheck(); + return this; + } + + /** Run an immediate check and schedule recurring checks at the configured interval. */ + public ModrinthUpdateChecker startPeriodicCheck() { + checkNow(); + long intervalTicks = checkIntervalHours * 60 * 60 * 20L; + UniversalScheduler.getScheduler(plugin) + .runTaskTimerAsynchronously(this::performCheck, intervalTicks, intervalTicks); + return this; + } + + // ------------------------------------------------------------------------- + // Player notification API — consumed by PlayerJoinListener + // ------------------------------------------------------------------------- + + /** + * Returns whether this player should receive an update notification. + * Called by {@link org.milkteamc.autotreechop.events.PlayerJoinListener}. + */ + public boolean shouldNotifyPlayer(@NotNull Player player) { + if (lastResult != UpdateCheckResult.NEW_VERSION_AVAILABLE) return false; + return (notifyOps && player.isOp()) || (notifyPermission != null && player.hasPermission(notifyPermission)); + } + + /** + * Send the update notification message to a player. + * Routes through {@link org.milkteamc.autotreechop.translation.TranslationManager}'s + * {@link net.kyori.adventure.platform.bukkit.BukkitAudiences} to avoid class loader conflicts + * with the shaded Adventure library. + * Called by {@link org.milkteamc.autotreechop.events.PlayerJoinListener}. + */ + public void notifyPlayer(@NotNull Player player) { + if (lastResult != UpdateCheckResult.NEW_VERSION_AVAILABLE) return; + + Audience audience = plugin.getTranslationManager().getAdventure().player(player); + + audience.sendMessage(Component.text("There is a new version of ") + .color(NamedTextColor.GRAY) + .append(Component.text(plugin.getName()).color(NamedTextColor.GOLD)) + .append(Component.text(" available.").color(NamedTextColor.GRAY))); + + buildLinkBar().ifPresent(audience::sendMessage); + + audience.sendMessage(Component.text("Latest: ") + .color(NamedTextColor.DARK_GRAY) + .append(Component.text(latestVersion).color(NamedTextColor.GREEN)) + .append(Component.text(" | Your version: ").color(NamedTextColor.DARK_GRAY)) + .append(Component.text(currentVersion).color(NamedTextColor.RED))); + } + + // ------------------------------------------------------------------------- + // Internal — HTTP check + // ------------------------------------------------------------------------- + + private void performCheck() { + try { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(API_URL.replace("{id}", projectId))) + .header("User-Agent", "Java-HttpClient " + plugin.getName() + "/" + currentVersion) + .GET() + .build(); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAcceptAsync(response -> { + if (response.statusCode() != 200) { + lastResult = UpdateCheckResult.UNKNOWN; + plugin.getLogger() + .warning("Failed to check for updates (HTTP " + response.statusCode() + ")"); + return; + } + try { + JsonArray versions = + JsonParser.parseString(response.body()).getAsJsonArray(); + String latest = getLatestVersion(versions); + + if (latest == null) { + lastResult = UpdateCheckResult.UNKNOWN; + return; + } + + latestVersion = latest; + lastResult = compareVersions(getRawVersion(latest), getRawVersion(currentVersion)) > 0 + ? UpdateCheckResult.NEW_VERSION_AVAILABLE + : UpdateCheckResult.RUNNING_LATEST_VERSION; + + UniversalScheduler.getScheduler(plugin).runTask(this::printResultToConsole); + + } catch (Exception e) { + lastResult = UpdateCheckResult.UNKNOWN; + plugin.getLogger().log(Level.WARNING, "Error parsing update check response", e); + } + }) + .exceptionally(t -> { + lastResult = UpdateCheckResult.UNKNOWN; + return null; + }); + } catch (Exception e) { + lastResult = UpdateCheckResult.UNKNOWN; + } + } + + @Nullable + private String getLatestVersion(JsonArray versions) { + return versions.asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(v -> "release".equalsIgnoreCase(v.get("version_type").getAsString())) + .filter(this::isVersionCompatible) + .map(v -> v.get("version_number").getAsString()) + .map(ModrinthUpdateChecker::getRawVersion) + .max(ModrinthUpdateChecker::compareVersions) + .orElse(null); + } + + private boolean isVersionCompatible(JsonObject version) { + JsonArray gameVersions = version.get("game_versions").getAsJsonArray(); + JsonArray loaders = version.get("loaders").getAsJsonArray(); + return (minecraftVersion == null || gameVersions.contains(new JsonPrimitive(minecraftVersion))) + && loaders.contains(new JsonPrimitive(loader)); + } + + // ------------------------------------------------------------------------- + // Internal — Console output (plain JUL, avoids shaded Adventure conflict) + // ------------------------------------------------------------------------- + + private void printResultToConsole() { + switch (lastResult) { + case UNKNOWN -> plugin.getLogger().warning("Could not check for updates."); + case RUNNING_LATEST_VERSION -> { + if (suppressUpToDateMessage) return; + plugin.getLogger().info("You are running the latest version of " + plugin.getName() + "."); + } + case NEW_VERSION_AVAILABLE -> printUpdateBoxToConsole(); + } + } + + private void printUpdateBoxToConsole() { + List lines = new ArrayList<>(); + + lines.add("A new version of " + plugin.getName() + " is available!"); + lines.add(""); + lines.add("Your version: " + currentVersion); + lines.add("Latest version: " + latestVersion); + + if (downloadLink != null) { + lines.add(""); + lines.add("Download: " + downloadLink); + } + if (supportLink != null) { + lines.add("Support: " + supportLink); + } + if (donationLink != null) { + lines.add("Donate: " + donationLink); + } + + String border = "*".repeat(60); + plugin.getLogger().warning(border); + for (String line : lines) { + plugin.getLogger().warning("* " + line); + } + plugin.getLogger().warning(border); + } + + // ------------------------------------------------------------------------- + // Internal — clickable link bar for player messages + // ------------------------------------------------------------------------- + + private Optional buildLinkBar() { + record Link(String label, String url) {} + + List links = new ArrayList<>(); + if (downloadLink != null) links.add(new Link("Download", downloadLink)); + if (donationLink != null) links.add(new Link("Donate", donationLink)); + if (changelogLink != null) links.add(new Link("Changelog", changelogLink)); + if (supportLink != null) links.add(new Link("Support", supportLink)); + + if (links.isEmpty()) return Optional.empty(); + + Component separator = Component.text(" | ").color(NamedTextColor.GRAY); + Component bar = Component.empty(); + Iterator it = links.iterator(); + while (it.hasNext()) { + Link link = it.next(); + Component btn = Component.text(link.label()) + .color(NamedTextColor.GOLD) + .decorate(TextDecoration.BOLD) + .clickEvent(ClickEvent.openUrl(link.url())) + .hoverEvent(HoverEvent.showText(Component.text("Link: ") + .color(NamedTextColor.GRAY) + .append(Component.text(link.url()).color(NamedTextColor.AQUA)))); + bar = bar.append(btn); + if (it.hasNext()) bar = bar.append(separator); + } + + return Optional.of(bar); + } + + // ------------------------------------------------------------------------- + // Internal — version parsing + // ------------------------------------------------------------------------- + + private static String getRawVersion(String version) { + if (version.isEmpty()) return version; + version = version.replaceAll("^\\D+", ""); + return version.split("\\+")[0]; + } + + /** Returns positive if v1 > v2, negative if v1 < v2, 0 if equal. */ + private static int compareVersions(String v1, String v2) { + String[] p1 = v1.split("\\."); + String[] p2 = v2.split("\\."); + int len = Math.max(p1.length, p2.length); + for (int i = 0; i < len; i++) { + int a = i < p1.length ? parseVersionPart(p1[i]) : 0; + int b = i < p2.length ? parseVersionPart(p2[i]) : 0; + if (a != b) return Integer.compare(a, b); + } + boolean v1Pre = v1.matches(".*(?i)(alpha|snapshot|beta|dev|rc).*"); + boolean v2Pre = v2.matches(".*(?i)(alpha|snapshot|beta|dev|rc).*"); + if (v1Pre && !v2Pre) return -1; + if (!v1Pre && v2Pre) return 1; + return 0; + } + + private static int parseVersionPart(String part) { + try { + return Integer.parseInt(part.replaceAll("[^0-9]", "")); + } catch (NumberFormatException e) { + return 0; + } + } + + // ------------------------------------------------------------------------- + // Accessors + // ------------------------------------------------------------------------- + + public UpdateCheckResult getLastResult() { + return lastResult; + } + + @Nullable + public String getLatestVersion() { + return latestVersion; + } + + public String getCurrentVersion() { + return currentVersion; + } +} From 74d1a2c0d3cf462b180d0093d421c54405aa81ad Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:10:37 +0800 Subject: [PATCH 47/55] some small improve to database --- .../database/DatabaseManager.java | 111 +++++++++++------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java index 9b71380..c4a37f2 100644 --- a/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java +++ b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java @@ -23,6 +23,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.time.LocalDate; import java.util.Map; import java.util.UUID; @@ -33,6 +34,7 @@ public class DatabaseManager { private final Plugin plugin; private final HikariDataSource dataSource; + private final boolean useMysql; public DatabaseManager( Plugin plugin, @@ -43,6 +45,7 @@ public DatabaseManager( String username, String password) { this.plugin = plugin; + this.useMysql = useMysql; this.dataSource = initializeDataSource(useMysql, hostname, port, database, username, password); createTable(); } @@ -56,7 +59,8 @@ private HikariDataSource initializeDataSource( config.setUsername(username); config.setPassword(password); } else { - config.setJdbcUrl("jdbc:sqlite:plugins/AutoTreeChop/player_data.db"); + String dbPath = plugin.getDataFolder().getAbsolutePath() + "/player_data.db"; + config.setJdbcUrl("jdbc:sqlite:" + dbPath); } config.setMaximumPoolSize(10); @@ -70,13 +74,13 @@ private HikariDataSource initializeDataSource( private void createTable() { try (Connection conn = dataSource.getConnection(); - PreparedStatement stmt = conn.prepareStatement( - "CREATE TABLE IF NOT EXISTS player_data (" + "uuid VARCHAR(36) PRIMARY KEY," - + "autoTreeChopEnabled BOOLEAN," - + "dailyUses INT," - + "dailyBlocksBroken INT," - + "lastUseDate VARCHAR(10))")) { - stmt.executeUpdate(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS player_data (" + + "uuid VARCHAR(36) PRIMARY KEY," + + "autoTreeChopEnabled BOOLEAN," + + "dailyUses INT," + + "dailyBlocksBroken INT," + + "lastUseDate VARCHAR(10))"); } catch (SQLException e) { plugin.getLogger().warning("Error creating database table: " + e.getMessage()); } @@ -88,19 +92,19 @@ public CompletableFuture loadPlayerDataAsync(UUID playerUUID, boolea PreparedStatement stmt = conn.prepareStatement("SELECT * FROM player_data WHERE uuid = ?")) { stmt.setString(1, playerUUID.toString()); - ResultSet rs = stmt.executeQuery(); - - if (rs.next()) { - return new PlayerData( - playerUUID, - rs.getBoolean("autoTreeChopEnabled"), - rs.getInt("dailyUses"), - rs.getInt("dailyBlocksBroken"), - LocalDate.parse(rs.getString("lastUseDate"))); - } else { - PlayerData data = new PlayerData(playerUUID, defaultTreeChop, 0, 0, LocalDate.now()); - insertPlayerData(data); - return data; + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return new PlayerData( + playerUUID, + rs.getBoolean("autoTreeChopEnabled"), + rs.getInt("dailyUses"), + rs.getInt("dailyBlocksBroken"), + LocalDate.parse(rs.getString("lastUseDate"))); + } else { + PlayerData data = new PlayerData(playerUUID, defaultTreeChop, 0, 0, LocalDate.now()); + insertPlayerData(data); + return data; + } } } catch (SQLException e) { plugin.getLogger().warning("Error loading player data: " + e.getMessage()); @@ -110,21 +114,11 @@ public CompletableFuture loadPlayerDataAsync(UUID playerUUID, boolea } public void savePlayerDataSync(PlayerData data) { + String sql = buildUpsertSql(); try (Connection conn = dataSource.getConnection(); - PreparedStatement stmt = - conn.prepareStatement("UPDATE player_data SET autoTreeChopEnabled = ?, dailyUses = ?, " - + "dailyBlocksBroken = ?, lastUseDate = ? WHERE uuid = ?")) { - - stmt.setBoolean(1, data.isAutoTreeChopEnabled()); - stmt.setInt(2, data.getDailyUses()); - stmt.setInt(3, data.getDailyBlocksBroken()); - stmt.setString(4, data.getLastUseDate().toString()); - stmt.setString(5, data.getPlayerUUID().toString()); - - int rows = stmt.executeUpdate(); - if (rows == 0) { - insertPlayerData(data); - } + PreparedStatement stmt = conn.prepareStatement(sql)) { + bindUpsertParams(stmt, data); + stmt.executeUpdate(); } catch (SQLException e) { plugin.getLogger().warning("Error saving player data: " + e.getMessage()); } @@ -134,22 +128,15 @@ public CompletableFuture savePlayerDataBatchAsync(Map da return CompletableFuture.runAsync(() -> { if (dataMap.isEmpty()) return; + String sql = buildUpsertSql(); try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); - try (PreparedStatement stmt = - conn.prepareStatement("UPDATE player_data SET autoTreeChopEnabled = ?, dailyUses = ?, " - + "dailyBlocksBroken = ?, lastUseDate = ? WHERE uuid = ?")) { - + try (PreparedStatement stmt = conn.prepareStatement(sql)) { for (PlayerData data : dataMap.values()) { - stmt.setBoolean(1, data.isAutoTreeChopEnabled()); - stmt.setInt(2, data.getDailyUses()); - stmt.setInt(3, data.getDailyBlocksBroken()); - stmt.setString(4, data.getLastUseDate().toString()); - stmt.setString(5, data.getPlayerUUID().toString()); + bindUpsertParams(stmt, data); stmt.addBatch(); } - stmt.executeBatch(); conn.commit(); } catch (SQLException e) { @@ -162,6 +149,40 @@ public CompletableFuture savePlayerDataBatchAsync(Map da }); } + /** + * Returns a dialect-appropriate UPSERT statement. + * + *
    + *
  • SQLite: {@code INSERT OR REPLACE INTO ...} + *
  • MySQL: {@code INSERT INTO ... ON DUPLICATE KEY UPDATE ...} + *
+ */ + private String buildUpsertSql() { + if (useMysql) { + return "INSERT INTO player_data (uuid, autoTreeChopEnabled, dailyUses, dailyBlocksBroken, lastUseDate) " + + "VALUES (?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE " + + "autoTreeChopEnabled = VALUES(autoTreeChopEnabled), " + + "dailyUses = VALUES(dailyUses), " + + "dailyBlocksBroken = VALUES(dailyBlocksBroken), " + + "lastUseDate = VALUES(lastUseDate)"; + } else { + // SQLite: INSERT OR REPLACE replaces the entire row when the PK conflicts. + return "INSERT OR REPLACE INTO player_data " + + "(uuid, autoTreeChopEnabled, dailyUses, dailyBlocksBroken, lastUseDate) " + + "VALUES (?, ?, ?, ?, ?)"; + } + } + + /** Binds the five UPSERT parameters in the order declared by {@link #buildUpsertSql()}. */ + private void bindUpsertParams(PreparedStatement stmt, PlayerData data) throws SQLException { + stmt.setString(1, data.getPlayerUUID().toString()); + stmt.setBoolean(2, data.isAutoTreeChopEnabled()); + stmt.setInt(3, data.getDailyUses()); + stmt.setInt(4, data.getDailyBlocksBroken()); + stmt.setString(5, data.getLastUseDate().toString()); + } + private void insertPlayerData(PlayerData data) throws SQLException { try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = From d00d79a6d62b4e95b3dfd75f16a04e4367fa3364 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:17:33 +0800 Subject: [PATCH 48/55] some more small fixes --- .../events/BlockBreakListener.java | 42 ++++--- .../autotreechop/utils/TreeChopUtils.java | 116 +++++------------- 2 files changed, 56 insertions(+), 102 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index b142913..8f7f2da 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -121,7 +121,7 @@ public void onBlockBreak(BlockBreakEvent event) { return; } - // Limits cleared — now check for a pending confirmation. + // Limits cleared — check for a pending confirmation first. ConfirmationManager confirmationManager = plugin.getConfirmationManager(); ChopData pending = confirmationManager.consumePendingConfirmation(playerUUID); @@ -145,8 +145,8 @@ public void onBlockBreak(BlockBreakEvent event) { // here), then read them on an async thread (snapshots are immutable — thread-safe). Map snapshots = captureLeafCheckSnapshots(block, config); - // Clone location and tool now so we have stable values if the async path - // later needs them (block reference is live world state — not safe async). + // Clone location and tool now so we have stable values for the async path. + // The block reference itself is a live world object — not safe to read off-thread. Location frozenLocation = location.clone(); ItemStack frozenTool = tool.clone(); @@ -193,15 +193,7 @@ void dispatchChop( EffectUtils.showChopEffect(player, block); } - ProtectionHooks hooks = new ProtectionHooks( - plugin.isWorldGuardEnabled(), - plugin.getWorldGuardHook(), - plugin.isResidenceEnabled(), - plugin.getResidenceHook(), - plugin.isGriefPreventionEnabled(), - plugin.getGriefPreventionHook(), - plugin.isLandsEnabled(), - plugin.getLandsHook()); + ProtectionHooks hooks = buildProtectionHooks(); plugin.getTreeChopUtils() .chopTree( @@ -215,6 +207,24 @@ 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() { + return new ProtectionHooks( + plugin.isWorldGuardEnabled(), + plugin.getWorldGuardHook(), + plugin.isResidenceEnabled(), + plugin.getResidenceHook(), + plugin.isGriefPreventionEnabled(), + plugin.getGriefPreventionHook(), + plugin.isLandsEnabled(), + plugin.getLandsHook()); + } + /** * Captures {@link ChunkSnapshot}s for all chunks within the leaf-detection radius. * @@ -233,7 +243,7 @@ private Map captureLeafCheckSnapshots(Block log, Config con int chunkX = (cx + dx) >> 4; int chunkZ = (cz + dz) >> 4; if (!world.isChunkLoaded(chunkX, chunkZ)) continue; - long key = ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL); + long key = chunkKey(chunkX, chunkZ); snapshots.computeIfAbsent( key, k -> world.getChunkAt(chunkX, chunkZ).getChunkSnapshot(false, false, false)); } @@ -266,7 +276,7 @@ private static boolean hasNearbyLeaves(Block log, Config config, Map= maxY) continue; - long key = ((long) (x >> 4) << 32) | ((z >> 4) & 0xFFFFFFFFL); + long key = chunkKey(x >> 4, z >> 4); ChunkSnapshot snapshot = snapshots.get(key); if (snapshot == null) continue; @@ -278,4 +288,8 @@ private static boolean hasNearbyLeaves(Block log, Config config, Map 0) { - estimatedDamage = blockCount / (unbreakingLevel + 1); + estimatedDamage = (blockCount * damagePerHit) / (unbreakingLevel + 1); } else { - estimatedDamage = blockCount * config.getToolDamageDecrease(); + estimatedDamage = blockCount * damagePerHit; } return remainingDurability > estimatedDamage; @@ -98,7 +99,7 @@ private static void applyToolDamage(ItemStack tool, Player player, int blocksBro int newDamage = currentDamage + damageToApply; if (newDamage >= tool.getType().getMaxDurability()) { - player.getInventory().removeItem(tool); + player.getInventory().setItemInMainHand(null); } else { damageableMeta.setDamage(newDamage); tool.setItemMeta(damageableMeta); @@ -116,7 +117,7 @@ private static boolean shouldApplyDurabilityLoss(int unbreakingLevel, Config con if (unbreakingLevel <= 0 || !config.getRespectUnbreaking()) { return true; } - return random.nextInt(100) < (100.0 / (unbreakingLevel + 1)); + return random.nextInt(unbreakingLevel + 1) == 0; } public static boolean isTool(Player player) { @@ -140,10 +141,6 @@ public static boolean isTool(Player player) { return xMat == XMaterial.SHEARS || xMat == XMaterial.FISHING_ROD || xMat == XMaterial.FLINT_AND_STEEL; } - /** - * Main entry point for tree chopping - * PHASE 1: Synchronous snapshot creation - */ public void chopTree( Block block, Player player, @@ -180,14 +177,12 @@ public void chopTree( // Mark location as processing sessionManager.addTreeChopLocations(playerUUID, Collections.singleton(block.getLocation())); - // PHASE 1: Synchronous - Capture block snapshot try { BlockSnapshot treeSnapshot = BlockSnapshotCreator.captureTreeRegion( block, config, connectedBlocks, config.getMaxDiscoveryBlocks()); Location startLocation = block.getLocation().clone(); - // PHASE 2: Asynchronous - Calculate tree structure Runnable asyncDiscovery = () -> { try { Set treeBlocks = BlockDiscoveryUtils.discoverTreeBFS( @@ -215,10 +210,6 @@ public void chopTree( } } - /** - * Validate tree and execute chopping - * This runs synchronously on the region thread - */ private void validateAndExecuteChop( Set treeBlocks, Block originalBlock, @@ -235,7 +226,6 @@ private void validateAndExecuteChop( return; } - // Validation checks if (treeBlocks.isEmpty()) { sessionManager.clearTreeChopSession(playerUUID); return; @@ -267,10 +257,6 @@ private void validateAndExecuteChop( executeTreeChop(treeBlocks, player, tool, config, playerConfig, hooks, originalBlock); } - /** - * Execute tree chopping in batches - * This runs synchronously with batch processing - */ private void executeTreeChop( Set treeBlocks, Player player, @@ -285,13 +271,10 @@ private void executeTreeChop( int totalBlocks = blockList.size(); UUID playerUUID = player.getUniqueId(); - // Track the LOWEST log of each type for replanting (Y coordinate) + Location centerLocation = originalBlock.getLocation().clone(); Map logTypesForReplant = new HashMap<>(); - // Use thread-safe set for actuallyRemovedLogs since it's accessed across batches Set actuallyRemovedLogs = ConcurrentHashMap.newKeySet(); - // CRITICAL: Capture leaf snapshot BEFORE removing logs - // This ensures we can see which logs exist for proper leaf orphan detection BlockSnapshot leafSnapshot = null; if (config.isLeafRemovalEnabled()) { try { @@ -311,7 +294,7 @@ private void executeTreeChop( (location, index) -> { Block block = location.getBlock(); - // Re-check block type (may have changed) + // Re-check block type (may have changed between phases) if (!BlockDiscoveryUtils.isLog(block.getType(), config)) { return; } @@ -323,7 +306,7 @@ private void executeTreeChop( Material originalLogType = block.getType(); - // Track the lowest Y coordinate log for each type (for proper replanting) + // Track the lowest-Y log of each type for replanting Location existingLoc = logTypesForReplant.get(originalLogType); if (existingLoc == null || location.getBlockY() < existingLoc.getBlockY()) { logTypesForReplant.put(originalLogType, location.clone()); @@ -355,20 +338,17 @@ private void executeTreeChop( // Handle leaf removal if (config.isLeafRemovalEnabled() && finalLeafSnapshot != null) { long delay = config.getLeafRemovalDelayTicks(); - Location leafProcessLocation = originalBlock.getLocation(); - Runnable leafTask = () -> { - processLeafRemovalWithPreCapturedSnapshot( - finalLeafSnapshot, - originalBlock.getLocation(), - player, - config, - playerConfig, - hooks, - actuallyRemovedLogs); - }; + Runnable leafTask = () -> processLeafRemovalWithPreCapturedSnapshot( + finalLeafSnapshot, + centerLocation, + player, + config, + playerConfig, + hooks, + actuallyRemovedLogs); - scheduler.scheduleDelayed(leafProcessLocation, leafTask, delay); + scheduler.scheduleDelayed(centerLocation, leafTask, delay); } // Handle replanting @@ -399,13 +379,6 @@ private void executeTreeChop( }); } - /** - * Process leaf removal with PRE-CAPTURED snapshot - * The snapshot was taken BEFORE logs were removed - * PHASE 1: Already done (snapshot captured before log removal) - * PHASE 2: Async - Calculate leaves to remove - * PHASE 3: Sync - Remove leaves in batches - */ private void processLeafRemovalWithPreCapturedSnapshot( BlockSnapshot leafSnapshot, Location centerLocation, @@ -425,36 +398,28 @@ private void processLeafRemovalWithPreCapturedSnapshot( String playerKey = player.getUniqueId().toString(); - // Check if player already has an active leaf removal session if (sessionManager.hasActiveLeafRemovalSession(playerKey)) { return; } - // Start a new session String sessionId = sessionManager.startLeafRemovalSession(playerKey); if (sessionId == null) { return; } - // PHASE 2: Asynchronous - Calculate leaves to remove Runnable asyncLeafCalculation = () -> { try { - // Use the provided removedLogs directly - // (already contains all actually removed logs from executeTreeChop) Set leavesToRemove; int radius = config.getLeafRemovalRadius(); - // Choose discovery method based on radius and mode if ("smart".equalsIgnoreCase(config.getLeafRemovalMode())) { leavesToRemove = BlockDiscoveryUtils.discoverLeavesBFS( leafSnapshot, centerLocation, radius, config, removedLogs); } else { - // For "aggressive" or "radius" mode, radial is faster leavesToRemove = BlockDiscoveryUtils.discoverLeavesRadial( leafSnapshot, centerLocation, radius, config, removedLogs); } - // PHASE 3: Back to sync for removal Runnable removalTask = () -> executeLeafRemoval(leavesToRemove, player, config, playerConfig, hooks, sessionId, playerKey); @@ -470,15 +435,10 @@ private void processLeafRemovalWithPreCapturedSnapshot( if (config.isLeafRemovalAsync()) { scheduler.runTaskAsync(asyncLeafCalculation); } else { - // Run synchronously if async is disabled asyncLeafCalculation.run(); } } - /** - * Execute leaf removal in batches - * This runs synchronously on the region thread - */ private void executeLeafRemoval( Set leavesToRemove, Player player, @@ -501,31 +461,20 @@ private void executeLeafRemoval( 0, batchSize, (location, index) -> { - // Check daily limit if counting towards limit if (config.getLeafRemovalCountsTowardsLimit()) { if (!PermissionUtils.hasVipBlock(player, playerConfig, config) && playerConfig.getDailyBlocksBroken() >= config.getMaxBlocksPerDay()) { - return false; // Stop processing - limit reached + return false; } } Block leafBlock = location.getBlock(); - - // Remove the leaf block with all checks removeLeafBlock(leafBlock, player, config, playerConfig, hooks); - - return true; // Continue processing + return true; }, - () -> { - // Leaf removal complete - end session - sessionManager.endLeafRemovalSession(sessionId, playerKey); - }); + () -> sessionManager.endLeafRemovalSession(sessionId, playerKey)); } - /** - * Remove a single leaf block with all necessary checks - * This runs synchronously on the region thread - */ private boolean removeLeafBlock( Block leafBlock, Player player, @@ -535,26 +484,19 @@ private boolean removeLeafBlock( Location leafLocation = leafBlock.getLocation(); - // Check if already processing this location - if (processingLeafLocations.contains(leafLocation)) { - return false; - } - - // Re-check if it's still a leaf - if (!BlockDiscoveryUtils.isLeafBlock(leafBlock.getType(), config)) { + if (!processingLeafLocations.add(leafLocation)) { return false; } - // Re-check protection at execution time - if (!ProtectionCheckUtils.canModifyBlock(player, leafLocation, hooks)) { - return false; - } + try { + if (!BlockDiscoveryUtils.isLeafBlock(leafBlock.getType(), config)) { + return false; + } - // Mark as processing - processingLeafLocations.add(leafLocation); + if (!ProtectionCheckUtils.canModifyBlock(player, leafLocation, hooks)) { + return false; + } - try { - // Call BlockBreakEvent if enabled if (config.isCallBlockBreakEvent()) { BlockBreakEvent breakEvent = new BlockBreakEvent(leafBlock, player); plugin.getServer().getPluginManager().callEvent(breakEvent); @@ -573,7 +515,6 @@ private boolean removeLeafBlock( leafBlock.setType(XMaterial.AIR.get(), false); } - // Update daily blocks count if needed if (config.getLeafRemovalCountsTowardsLimit()) { playerConfig.incrementDailyBlocksBroken(); } @@ -581,7 +522,6 @@ private boolean removeLeafBlock( return true; } finally { - // Always remove from processing set processingLeafLocations.remove(leafLocation); } } From 65cf72a0c863eddc74784760c6d671263e74fd38 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:33:22 +0800 Subject: [PATCH 49/55] edit translation --- src/main/resources/lang/zh.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/lang/zh.properties b/src/main/resources/lang/zh.properties index ba40d33..9a4694f 100644 --- a/src/main/resources/lang/zh.properties +++ b/src/main/resources/lang/zh.properties @@ -36,6 +36,6 @@ alreadyDisabled=自動砍樹已經是停用狀態。 consoleName=控制台 aboutHeader=AutoTreeChop - v{version} 由 MilkTeaMC 團隊與貢獻者開發 -aboutLicense=授權條款:GNU General Public License v3.0 (GPL-3.0) +aboutLicense=授權條款:GNU 通用公眾授權條款 v3.0 (GPL-3.0) aboutGithub=GitHub:https://github.com/milkteamc/autotreechop aboutModrinth=Modrinth:https://modrinth.com/plugin/autotreechop From f5aa7ecf2185d48fd221960c54cb7b1a4ae5a8ca Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:34:17 +0800 Subject: [PATCH 50/55] Stop using some of deprecated APIs --- .../translation/TranslationManager.java | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java b/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java index 5eb2a96..d2054db 100644 --- a/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java +++ b/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java @@ -249,17 +249,8 @@ private Locale parseLocale(String localeCode) { if (localeCode == null || localeCode.isEmpty()) { return null; } - - String[] parts = localeCode.split("_"); - if (parts.length == 1) { - return new Locale(parts[0]); - } else if (parts.length == 2) { - return new Locale(parts[0], parts[1]); - } else if (parts.length == 3) { - return new Locale(parts[0], parts[1], parts[2]); - } - - return null; + String languageTag = localeCode.replace('_', '-'); + return Locale.forLanguageTag(languageTag); } /** @@ -267,21 +258,15 @@ private Locale parseLocale(String localeCode) { */ public Locale getLocale(CommandSender sender) { if (useClientLocale && sender instanceof Player player) { - String clientLocale = player.getLocale(); + Locale clientLocale = player.locale(); - // Try exact match first (e.g., zh_TW) - Locale locale = parseLocale(clientLocale); - if (locale != null && translations.containsKey(locale)) { - return locale; + if (translations.containsKey(clientLocale)) { + return clientLocale; } - // Try just the language part (e.g., "zh" from "zh_TW") - if (clientLocale.contains("_")) { - String language = clientLocale.split("_")[0]; - locale = new Locale(language); - if (translations.containsKey(locale)) { - return locale; - } + Locale languageOnly = Locale.forLanguageTag(clientLocale.getLanguage()); + if (translations.containsKey(languageOnly)) { + return languageOnly; } } From 13efdd07ec0278ace55c62a2325f57369b41c96c Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:36:35 +0800 Subject: [PATCH 51/55] i forgot to change back version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 60b2c17..b46a173 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'org.milkteamc' -version = '1.7.2' +version = '1.7.4' // Java Configuration java { From 1ab71091d95db33f9dec6719c66793b3e5bfc80f Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 21:47:37 +0800 Subject: [PATCH 52/55] no more useless deprecation message --- .../org/milkteamc/autotreechop/AutoTreeChop.java | 13 +++++++++++++ .../autotreechop/AutoTreeChopExpansion.java | 4 ++-- .../autotreechop/command/AboutCommand.java | 2 +- .../autotreechop/updater/ModrinthUpdateChecker.java | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index b965e39..a215a58 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -28,6 +28,7 @@ import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; +import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; import org.milkteamc.autotreechop.command.AboutCommand; import org.milkteamc.autotreechop.command.ConfirmCommand; @@ -66,6 +67,7 @@ public class AutoTreeChop extends JavaPlugin { private TranslationManager translationManager; private ConfirmationManager confirmationManager; private ModrinthUpdateChecker updateChecker; + private PluginDescriptionFile description; private boolean worldGuardEnabled = false; private boolean residenceEnabled = false; @@ -101,6 +103,13 @@ public static boolean isFolia() { } } + @Override + public void onLoad() { + @SuppressWarnings("deprecation") + PluginDescriptionFile desc = getDescription(); + this.description = desc; + } + @Override public void onEnable() { instance = this; @@ -336,6 +345,10 @@ public ModrinthUpdateChecker getUpdateChecker() { return updateChecker; } + public PluginDescriptionFile getPluginDescription() { + return description; + } + public CooldownManager getCooldownManager() { return cooldownManager; } diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java index 05511f1..3108a38 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChopExpansion.java @@ -37,12 +37,12 @@ public AutoTreeChopExpansion(AutoTreeChop plugin) { @Override public @NotNull String getAuthor() { - return plugin.getDescription().getAuthors().get(0); + return plugin.getPluginDescription().getAuthors().get(0); } @Override public @NotNull String getVersion() { - return plugin.getDescription().getVersion(); + return plugin.getPluginDescription().getVersion(); } @Override diff --git a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java index 8ea8b89..07a8ee9 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java @@ -41,7 +41,7 @@ public void about(BukkitCommandActor actor) { AutoTreeChop.sendMessage( sender, MessageKeys.ABOUT_HEADER, - Placeholder.parsed("version", plugin.getDescription().getVersion())); + Placeholder.parsed("version", plugin.getPluginDescription().getVersion())); AutoTreeChop.sendMessage(sender, MessageKeys.ABOUT_LICENSE); AutoTreeChop.sendMessage(sender, MessageKeys.ABOUT_GITHUB); diff --git a/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java index eac8a81..8dce44b 100644 --- a/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java +++ b/src/main/java/org/milkteamc/autotreechop/updater/ModrinthUpdateChecker.java @@ -93,7 +93,7 @@ public enum UpdateCheckResult { public ModrinthUpdateChecker(@NotNull AutoTreeChop plugin, @NotNull String projectId, @NotNull String loader) { this.plugin = plugin; this.projectId = projectId; - this.currentVersion = plugin.getDescription().getVersion(); + this.currentVersion = plugin.getPluginDescription().getVersion(); this.loader = loader; this.minecraftVersion = plugin.getServer().getBukkitVersion().split("-")[0]; } From b1edd87190949aaf5c51d2db174be1b0ac0e2add Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 22:55:51 +0800 Subject: [PATCH 53/55] this probably fix spigot --- .../translation/TranslationManager.java | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java b/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java index d2054db..3eb7bf8 100644 --- a/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java +++ b/src/main/java/org/milkteamc/autotreechop/translation/TranslationManager.java @@ -35,6 +35,18 @@ */ public class TranslationManager { + private static final boolean HAS_PAPER_LOCALE_API; + + static { + boolean hasApi = false; + try { + Player.class.getMethod("locale"); + hasApi = true; + } catch (NoSuchMethodException e) { + } + HAS_PAPER_LOCALE_API = hasApi; + } + private final AutoTreeChop plugin; private final StyleRegistry styleRegistry; private final MessageFormatter formatter; @@ -143,11 +155,11 @@ private void updateTranslationFiles() { * *

Handles all characters that {@link Properties#load} treats specially: *

    - *
  • {@code \} → {@code \\}
  • - *
  • newline / carriage-return / tab → {@code \n} / {@code \r} / {@code \t}
  • - *
  • Leading spaces and form-feeds — prefixed with {@code \} so that - * {@code Properties.load()} does not strip them as whitespace before - * the value.
  • + *
  • {@code \} → {@code \\}
  • + *
  • newline / carriage-return / tab → {@code \n} / {@code \r} / {@code \t}
  • + *
  • Leading spaces and form-feeds — prefixed with {@code \} so that + * {@code Properties.load()} does not strip them as whitespace before + * the value.
  • *
* *

Note: {@code #} and {@code !} do not need escaping when they appear @@ -253,20 +265,32 @@ private Locale parseLocale(String localeCode) { return Locale.forLanguageTag(languageTag); } + private Locale getPlayerLocale(Player player) { + if (HAS_PAPER_LOCALE_API) { + return player.locale(); + } else { + @SuppressWarnings("deprecation") + String localeString = player.getLocale(); + return parseLocale(localeString); + } + } + /** * Gets the appropriate locale for a command sender */ public Locale getLocale(CommandSender sender) { if (useClientLocale && sender instanceof Player player) { - Locale clientLocale = player.locale(); + Locale clientLocale = getPlayerLocale(player); - if (translations.containsKey(clientLocale)) { - return clientLocale; - } + if (clientLocale != null) { + if (translations.containsKey(clientLocale)) { + return clientLocale; + } - Locale languageOnly = Locale.forLanguageTag(clientLocale.getLanguage()); - if (translations.containsKey(languageOnly)) { - return languageOnly; + Locale languageOnly = Locale.forLanguageTag(clientLocale.getLanguage()); + if (translations.containsKey(languageOnly)) { + return languageOnly; + } } } @@ -278,10 +302,10 @@ public Locale getLocale(CommandSender sender) { * *

Fallback priority: *

    - *
  1. Requested locale
  2. - *
  3. English ({@link Locale#ENGLISH}) — always the canonical reference translation
  4. - *
  5. The configured default locale (if different from English)
  6. - *
  7. Any loaded locale that contains the key
  8. + *
  9. Requested locale
  10. + *
  11. English ({@link Locale#ENGLISH}) — always the canonical reference translation
  12. + *
  13. The configured default locale (if different from English)
  14. + *
  15. Any loaded locale that contains the key
  16. *
*/ public String getMessage(String key, Locale locale) { From 79bf96d9952be73a40fe04a1d25c4e036d3cd8a0 Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 23:18:34 +0800 Subject: [PATCH 54/55] allow vanilla tree chopping when still in cooldown --- .../milkteamc/autotreechop/events/BlockBreakListener.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 8f7f2da..3c11e26 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -96,10 +96,6 @@ public void onBlockBreak(BlockBreakEvent event) { return; } - // Cancel the event now — from this point we own the block break. - // chopTree handles the actual breaking itself via breakNaturally(). - event.setCancelled(true); - if (plugin.getCooldownManager().isInCooldown(playerUUID)) { long remaining = plugin.getCooldownManager().getRemainingCooldown(playerUUID); AutoTreeChop.sendMessage( @@ -125,6 +121,8 @@ public void onBlockBreak(BlockBreakEvent event) { 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. From e53d9f0a698038955b4d57a798fa6ea9d94a8cfd Mon Sep 17 00:00:00 2001 From: MagicTeaMC Date: Sat, 11 Apr 2026 23:19:05 +0800 Subject: [PATCH 55/55] small codebase improvement --- .../milkteamc/autotreechop/AutoTreeChop.java | 58 +++++++------------ .../events/BlockBreakListener.java | 16 +++-- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java index a215a58..531a060 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -19,7 +19,6 @@ import com.github.Anon8281.universalScheduler.UniversalScheduler; import java.io.File; -import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.UUID; @@ -241,15 +240,10 @@ private void initializeHooks() { } private void loadLocale() { - saveResourceIfNotExists("lang/styles.properties"); - saveResourceIfNotExists("lang/en.properties"); - saveResourceIfNotExists("lang/de.properties"); - saveResourceIfNotExists("lang/es.properties"); - saveResourceIfNotExists("lang/fr.properties"); - saveResourceIfNotExists("lang/ja.properties"); - saveResourceIfNotExists("lang/ru.properties"); - saveResourceIfNotExists("lang/zh.properties"); - saveResourceIfNotExists("lang/ms.properties"); + 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()); @@ -273,11 +267,22 @@ public void onDisable() { } } - if (confirmationManager != null && playerConfigs != null) { + if (playerConfigs != null && !playerConfigs.isEmpty()) { + SessionManager sessionManager = SessionManager.getInstance(); for (Map.Entry entry : playerConfigs.entrySet()) { - confirmationManager.clearPlayer(entry.getKey()); - if (entry.getValue().isDirty()) { - databaseManager.savePlayerDataSync(entry.getValue().getData()); + 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(); @@ -287,13 +292,6 @@ public void onDisable() { databaseManager.close(); } - if (playerConfigs != null) { - SessionManager sessionManager = SessionManager.getInstance(); - for (UUID uuid : new HashSet<>(playerConfigs.keySet())) { - sessionManager.clearAllPlayerSessions(uuid); - } - } - if (translationManager != null) { translationManager.close(); } @@ -309,21 +307,9 @@ public PlayerConfig getPlayerConfig(UUID playerUUID) { PlayerConfig playerConfig = playerConfigs.get(playerUUID); if (playerConfig == null) { - getLogger().warning("PlayerConfig not found for " + playerUUID + ", loading synchronously"); - try { - DatabaseManager.PlayerData data = databaseManager - .loadPlayerDataAsync(playerUUID, config.getDefaultTreeChop()) - .get(); - - playerConfig = new PlayerConfig(playerUUID, data); - playerConfigs.put(playerUUID, playerConfig); - } catch (Exception e) { - getLogger().warning("Failed to load player data: " + e.getMessage()); - DatabaseManager.PlayerData defaultData = new DatabaseManager.PlayerData( - playerUUID, config.getDefaultTreeChop(), 0, 0, java.time.LocalDate.now()); - playerConfig = new PlayerConfig(playerUUID, defaultData); - playerConfigs.put(playerUUID, playerConfig); - } + DatabaseManager.PlayerData tempDefaultData = + new DatabaseManager.PlayerData(playerUUID, false, 0, 0, java.time.LocalDate.now()); + return new PlayerConfig(playerUUID, tempDefaultData); } return playerConfig; diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 3c11e26..12c6d9e 100644 --- a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java +++ b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java @@ -141,15 +141,15 @@ public void onBlockBreak(BlockBreakEvent event) { // 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). - Map snapshots = captureLeafCheckSnapshots(block, config); + int radius = config.getNoLeavesDetectionRadius(); + Map snapshots = captureLeafCheckSnapshots(block, radius); - // Clone location and tool now so we have stable values for the async path. - // The block reference itself is a live world object — not safe to read off-thread. - Location frozenLocation = location.clone(); + // 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, config, snapshots); + boolean hasLeaves = hasNearbyLeaves(block, radius, config, snapshots); // Return to the main/region thread to act on the result. scheduler.runTaskAtLocation(frozenLocation, () -> { @@ -229,8 +229,7 @@ private ProtectionHooks buildProtectionHooks() { *

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, Config config) { - int radius = config.getNoLeavesDetectionRadius(); + private Map captureLeafCheckSnapshots(Block log, int radius) { World world = log.getWorld(); int cx = log.getX(); int cz = log.getZ(); @@ -257,8 +256,7 @@ private Map captureLeafCheckSnapshots(Block log, Config con * pre-captured {@code snapshots}, which are immutable. Short-circuits on the * first leaf found. */ - private static boolean hasNearbyLeaves(Block log, Config config, Map snapshots) { - int radius = config.getNoLeavesDetectionRadius(); + private static boolean hasNearbyLeaves(Block log, int radius, Config config, Map snapshots) { World world = log.getWorld(); int cx = log.getX(); int cy = log.getY();