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" diff --git a/README.md b/README.md index 2781dac..64a0763 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # 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. -- 🌐 [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) @@ -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 @@ -64,8 +73,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 | --- @@ -100,17 +109,39 @@ 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) +- 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: `/plugins/bStats/config.yml` [![bstats](https://bstats.org/signatures/bukkit/AutoTreeChop.svg)](https://bstats.org/plugin/bukkit/AutoTreeChop/20053) + +--- + +## License + +```txt + 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 . +``` diff --git a/build.gradle b/build.gradle index 70177db..b46a173 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,19 @@ 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.+' } group = 'org.milkteamc' -version = '1.7.3' +version = '1.7.4' // Java Configuration java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } @@ -29,8 +29,9 @@ spotless { java { removeUnusedImports() formatAnnotations() - palantirJavaFormat() + + licenseHeaderFile(rootProject.file('spotless/HEADER')) } } @@ -70,24 +71,24 @@ 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' - 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' implementation 'org.bstats:bstats-bukkit:3.1.0' - implementation 'com.github.cryptomorin:XSeries: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.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' + compileOnly 'com.sk89q.worldguard:worldguard-bukkit:7.0.9' compileOnly 'com.github.GriefPrevention:GriefPrevention:16.18.4' } @@ -171,7 +172,11 @@ tasks.shadowJar { } tasks.runServer { - minecraftVersion('1.21.11') + minecraftVersion('26.1.1') + def javaVersion = Math.max(25, JavaVersion.current().majorVersion.toInteger()) + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(javaVersion) + } } runPaper.folia.registerTask() @@ -195,7 +200,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 @@ -206,7 +211,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', '26.1.2' ] loaders = ['paper', 'purpur', 'folia', 'spigot'] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a6..d997cfc 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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/. 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 9d3e41d..531a060 100644 --- a/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java +++ b/src/main/java/org/milkteamc/autotreechop/AutoTreeChop.java @@ -1,7 +1,24 @@ +/* + * 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 java.io.File; -import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.UUID; @@ -10,6 +27,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; @@ -27,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; @@ -35,35 +54,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 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 private static final int SAVE_THRESHOLD = 15; @@ -75,6 +65,8 @@ public class AutoTreeChop extends JavaPlugin { private Metrics metrics; private TranslationManager translationManager; private ConfirmationManager confirmationManager; + private ModrinthUpdateChecker updateChecker; + private PluginDescriptionFile description; private boolean worldGuardEnabled = false; private boolean residenceEnabled = false; @@ -110,6 +102,13 @@ public static boolean isFolia() { } } + @Override + public void onLoad() { + @SuppressWarnings("deprecation") + PluginDescriptionFile desc = getDescription(); + this.description = desc; + } + @Override public void onEnable() { instance = this; @@ -122,6 +121,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,23 +133,18 @@ 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."); } - 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, @@ -158,8 +156,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(); @@ -243,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()); @@ -268,32 +260,45 @@ 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()) { - confirmationManager.clearPlayer(entry.getKey()); - if (entry.getValue().isDirty()) { - databaseManager.savePlayerDataSync(entry.getValue().getData()); + if (playerConfigs != null && !playerConfigs.isEmpty()) { + SessionManager sessionManager = SessionManager.getInstance(); + for (Map.Entry entry : playerConfigs.entrySet()) { + UUID uuid = entry.getKey(); + PlayerConfig pConfig = entry.getValue(); + + if (confirmationManager != null) { + confirmationManager.clearPlayer(uuid); + } + + if (pConfig.isDirty() && databaseManager != null) { + databaseManager.savePlayerDataSync(pConfig.getData()); + } + + if (sessionManager != null) { + sessionManager.clearAllPlayerSessions(uuid); + } } + playerConfigs.clear(); } - playerConfigs.clear(); - if (databaseManager != null) { databaseManager.close(); } - 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!"); } @@ -302,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; @@ -334,6 +327,14 @@ public AutoTreeChopAPI getAutoTreeChopAPI() { return autoTreeChopAPI; } + public ModrinthUpdateChecker getUpdateChecker() { + return updateChecker; + } + + public PluginDescriptionFile getPluginDescription() { + return description; + } + public CooldownManager getCooldownManager() { return cooldownManager; } 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..3108a38 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; @@ -20,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/Config.java b/src/main/java/org/milkteamc/autotreechop/Config.java index 18987da..ded9b7e 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; @@ -9,16 +26,17 @@ 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; 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; @@ -95,6 +113,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 +163,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); @@ -228,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; @@ -496,23 +537,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; } 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/ModrinthUpdateChecker.java b/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java deleted file mode 100644 index b0a1f9a..0000000 --- a/src/main/java/org/milkteamc/autotreechop/ModrinthUpdateChecker.java +++ /dev/null @@ -1,465 +0,0 @@ -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 - plugin.getServer() - .getScheduler() - .runTaskTimerAsynchronously(plugin, 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) { - plugin.getServer() - .getScheduler() - .runTaskLater(plugin, () -> 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/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 925e557..07a8ee9 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/AboutCommand.java @@ -1,8 +1,26 @@ +/* + * 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; 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; @@ -22,12 +40,11 @@ public void about(BukkitCommandActor actor) { AutoTreeChop.sendMessage( sender, - AutoTreeChop.ABOUT_HEADER, - Placeholder.parsed("version", plugin.getDescription().getVersion())); + MessageKeys.ABOUT_HEADER, + Placeholder.parsed("version", plugin.getPluginDescription().getVersion())); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_LICENSE); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_GITHUB); - AutoTreeChop.sendMessage(sender, AutoTreeChop.ABOUT_DISCORD); - 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 08def6a..e488ec3 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; @@ -5,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; @@ -28,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; } @@ -40,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; } @@ -53,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/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..a564e55 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/ToggleCommand.java @@ -1,9 +1,27 @@ +/* + * 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; 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; @@ -35,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; } @@ -47,132 +65,131 @@ 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())); } } - // 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(), MessageKeys.NO_PERMISSION); 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(), MessageKeys.ONLY_PLAYERS); + return; + } + PlayerConfig playerConfig = plugin.getPlayerConfig(player.getUniqueId()); + if (playerConfig.isAutoTreeChopEnabled()) { + AutoTreeChop.sendMessage(player, MessageKeys.ALREADY_ENABLED); return; } + playerConfig.setAutoTreeChopEnabled(true); + AutoTreeChop.sendMessage(player, MessageKeys.ENABLED); + } + // 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( 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")); } } - // 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(), MessageKeys.NO_PERMISSION); 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(), MessageKeys.ONLY_PLAYERS); return; } + UUID playerUUID = player.getUniqueId(); + PlayerConfig playerConfig = plugin.getPlayerConfig(playerUUID); + if (!playerConfig.isAutoTreeChopEnabled()) { + AutoTreeChop.sendMessage(player, MessageKeys.ALREADY_DISABLED); + return; + } + playerConfig.setAutoTreeChopEnabled(false); + plugin.getConfirmationManager().clearPlayer(playerUUID); + AutoTreeChop.sendMessage(player, MessageKeys.DISABLED); + } + // 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++; 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; } @@ -182,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 3f57b18..46a0956 100644 --- a/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java +++ b/src/main/java/org/milkteamc/autotreechop/command/UsageCommand.java @@ -1,9 +1,27 @@ +/* + * 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; 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; @@ -24,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; } @@ -50,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/database/DatabaseManager.java b/src/main/java/org/milkteamc/autotreechop/database/DatabaseManager.java index bcdb242..c4a37f2 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; @@ -6,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; @@ -16,6 +34,7 @@ public class DatabaseManager { private final Plugin plugin; private final HikariDataSource dataSource; + private final boolean useMysql; public DatabaseManager( Plugin plugin, @@ -26,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(); } @@ -39,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); @@ -53,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()); } @@ -71,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()); @@ -93,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()); } @@ -117,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) { @@ -145,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 = diff --git a/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java b/src/main/java/org/milkteamc/autotreechop/events/BlockBreakListener.java index 6a7b181..12c6d9e 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; @@ -19,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; @@ -78,15 +96,11 @@ 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( player, - AutoTreeChop.STILL_IN_COOLDOWN_MESSAGE, + MessageKeys.STILL_IN_COOLDOWN, Placeholder.parsed("cooldown_time", String.valueOf(remaining))); return; } @@ -99,19 +113,21 @@ 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; } - // 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); + event.setCancelled(true); + if (pending != null) { // Player confirmed by breaking a log within the confirmation window. // Skip the leaf check entirely; grace is determined by the original reason. confirmationManager.recordSuccessfulChop(playerUUID, pending.reason(), false); - AutoTreeChop.sendMessage(player, AutoTreeChop.CONFIRMATION_SUCCESS_MESSAGE); + AutoTreeChop.sendMessage(player, MessageKeys.CONFIRMATION_SUCCESS); dispatchChop(player, playerConfig, block, tool, location, config); return; } @@ -125,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 if the async path - // later needs them (block reference is live world state — not safe async). - Location frozenLocation = location.clone(); + // Clone tool now so we have stable values for the async path. ItemStack frozenTool = tool.clone(); + Location frozenLocation = location; - plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - boolean hasLeaves = hasNearbyLeaves(block, config, snapshots); + scheduler.runTaskAsync(() -> { + boolean hasLeaves = hasNearbyLeaves(block, radius, config, snapshots); // Return to the main/region thread to act on the result. scheduler.runTaskAtLocation(frozenLocation, () -> { @@ -151,9 +167,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; @@ -175,15 +191,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( @@ -197,14 +205,31 @@ 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. * *

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(); @@ -215,7 +240,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)); } @@ -231,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(); @@ -248,7 +272,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; @@ -260,4 +284,8 @@ private static boolean hasNearbyLeaves(Block log, Config config, Map. + */ + 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; @@ -8,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 { @@ -44,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/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..7fc1c2a 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; @@ -6,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 { @@ -30,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/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..3eb7bf8 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.*; @@ -18,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; @@ -126,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 @@ -232,17 +261,18 @@ private Locale parseLocale(String localeCode) { if (localeCode == null || localeCode.isEmpty()) { return null; } + String languageTag = localeCode.replace('_', '-'); + return Locale.forLanguageTag(languageTag); + } - 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]); + private Locale getPlayerLocale(Player player) { + if (HAS_PAPER_LOCALE_API) { + return player.locale(); + } else { + @SuppressWarnings("deprecation") + String localeString = player.getLocale(); + return parseLocale(localeString); } - - return null; } /** @@ -250,20 +280,16 @@ private Locale parseLocale(String localeCode) { */ public Locale getLocale(CommandSender sender) { if (useClientLocale && sender instanceof Player player) { - String clientLocale = player.getLocale(); + Locale clientLocale = getPlayerLocale(player); - // Try exact match first (e.g., zh_TW) - Locale locale = parseLocale(clientLocale); - if (locale != null && translations.containsKey(locale)) { - return locale; - } + if (clientLocale != null) { + 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; } } } @@ -276,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) { 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..8dce44b --- /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.getPluginDescription().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; + } +} 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..7352448 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/EffectUtils.java @@ -1,8 +1,22 @@ +/* + * 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; -import static org.milkteamc.autotreechop.AutoTreeChop.sendMessage; - import com.cryptomorin.xseries.XMaterial; import com.cryptomorin.xseries.particles.ParticleDisplay; import com.cryptomorin.xseries.particles.XParticle; @@ -10,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) @@ -69,25 +85,22 @@ public static void showLeafRemovalEffect(Player player, Block block) { .spawn(); // Falling leaf-like block particles - if (XMaterial.supports(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()); } } } 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 3af15ff..0d5d94c 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeChopUtils.java @@ -1,5 +1,23 @@ +/* + * 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.XEnchantment; import com.cryptomorin.xseries.XMaterial; import com.cryptomorin.xseries.XSound; import java.util.*; @@ -7,13 +25,13 @@ 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; 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 { @@ -48,11 +66,12 @@ private static boolean hasEnoughDurability(ItemStack tool, int blockCount, Confi int unbreakingLevel = getUnbreakingLevel(tool); + int damagePerHit = config.isToolDamage() ? config.getToolDamageDecrease() : 1; int estimatedDamage; if (config.getRespectUnbreaking() && unbreakingLevel > 0) { - estimatedDamage = blockCount / (unbreakingLevel + 1); + estimatedDamage = (blockCount * damagePerHit) / (unbreakingLevel + 1); } else { - estimatedDamage = blockCount * config.getToolDamageDecrease(); + estimatedDamage = blockCount * damagePerHit; } return remainingDurability > estimatedDamage; @@ -80,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); @@ -89,7 +108,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; } @@ -98,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) { @@ -122,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, @@ -136,6 +151,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; @@ -157,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( @@ -192,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, @@ -207,21 +221,25 @@ private void validateAndExecuteChop( UUID playerUUID = player.getUniqueId(); - // Validation checks + if (!player.hasPermission("autotreechop.use")) { + sessionManager.clearTreeChopSession(playerUUID); + return; + } + if (treeBlocks.isEmpty()) { sessionManager.clearTreeChopSession(playerUUID); return; } 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; } @@ -239,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, @@ -257,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 { @@ -283,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; } @@ -295,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()); @@ -327,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 @@ -360,7 +368,8 @@ private void executeTreeChop( hooks.lands, hooks.residence, hooks.griefPrevention, - hooks.worldGuard); + hooks.worldGuard, + actuallyRemovedLogs); } } @@ -370,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, @@ -396,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); @@ -441,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, @@ -472,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, @@ -506,26 +484,19 @@ private boolean removeLeafBlock( Location leafLocation = leafBlock.getLocation(); - // Check if already processing this location - if (processingLeafLocations.contains(leafLocation)) { + if (!processingLeafLocations.add(leafLocation)) { return false; } - // Re-check if it's still a leaf - if (!BlockDiscoveryUtils.isLeafBlock(leafBlock.getType(), config)) { - 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); @@ -541,15 +512,9 @@ 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 if (config.getLeafRemovalCountsTowardsLimit()) { playerConfig.incrementDailyBlocksBroken(); } @@ -557,7 +522,6 @@ private boolean removeLeafBlock( return true; } finally { - // Always remove from processing set processingLeafLocations.remove(leafLocation); } } diff --git a/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java b/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java index 65a4cac..17c3f03 100644 --- a/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java +++ b/src/main/java/org/milkteamc/autotreechop/utils/TreeReplantUtils.java @@ -1,9 +1,28 @@ +/* + * 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; +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 +52,8 @@ public static void scheduleReplant( LandsHook landsHook, ResidenceHook residenceHook, GriefPreventionHook griefPreventionHook, - WorldGuardHook worldGuardHook) { + WorldGuardHook worldGuardHook, + Set choppedLogs) { if (!config.isAutoReplantEnabled()) { return; @@ -45,7 +65,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 +130,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; } /** diff --git a/src/main/resources/lang/de.properties b/src/main/resources/lang/de.properties index 4ae770e..898888c 100644 --- a/src/main/resources/lang/de.properties +++ b/src/main/resources/lang/de.properties @@ -10,3 +10,17 @@ 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. +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=Lizenz: GNU General Public License v3.0 (GPL-3.0) +aboutGithub=GitHub: https://github.com/milkteamc/autotreechop +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop diff --git a/src/main/resources/lang/en.properties b/src/main/resources/lang/en.properties index ee12fa6..3c1f47c 100644 --- a/src/main/resources/lang/en.properties +++ b/src/main/resources/lang/en.properties @@ -30,10 +30,12 @@ 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 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 e2bba6d..3563171 100644 --- a/src/main/resources/lang/es.properties +++ b/src/main/resources/lang/es.properties @@ -1,14 +1,28 @@ -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 +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop +alreadyEnabled=AutoTreeChop está activado. +alreadyDisabled=AutoTreeChop está desactivado. 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. diff --git a/src/main/resources/lang/it.properties b/src/main/resources/lang/it.properties new file mode 100644 index 0000000..8dfdd28 --- /dev/null +++ b/src/main/resources/lang/it.properties @@ -0,0 +1,26 @@ +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 +aboutModrinth=Modrinth: https://modrinth.com/plugin/autotreechop diff --git a/src/main/resources/lang/tr.properties b/src/main/resources/lang/tr.properties new file mode 100644 index 0000000..d1d0f47 --- /dev/null +++ b/src/main/resources/lang/tr.properties @@ -0,0 +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 diff --git a/src/main/resources/lang/zh.properties b/src/main/resources/lang/zh.properties index 9f4854e..9a4694f 100644 --- a/src/main/resources/lang/zh.properties +++ b/src/main/resources/lang/zh.properties @@ -30,10 +30,12 @@ noPendingConfirmation=目前沒有待確認的自動砍樹操作。 sneakEnabled=因進入潛行狀態,自動砍樹已啟用。 sneakDisabled=已離開潛行狀態,自動砍樹已停用。 +alreadyEnabled=自動砍樹已經是啟用狀態。 +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 -aboutDiscord=Discord:https://discord.gg/uQ4UXANnP2 aboutModrinth=Modrinth:https://modrinth.com/plugin/autotreechop