From 40faf769284f7ee8f9921954c73a7e56bc583fec Mon Sep 17 00:00:00 2001 From: bunchy7s Date: Sun, 21 Jun 2026 17:14:36 -0500 Subject: [PATCH] v1.1 - mcMMO Herbalism integration - Added reflection-based McMMOIntegration utility - Added McMMO_Integration toggle to config (default: true) - Added mcMMO Herbalism XP award per harvest (will try to read from mcMMO's experience.yml, i think) - Added Double Drops passive skill support (scales to level 1000) - Added Verdant Bounty passive skill support (scales to level 10000) - Added particle effects (HAPPY_VILLAGER + HEART) on skill procs --- README.md | 4 +- pom.xml | 6 +- .../test/listeners/CropHarvestListener.java | 60 +++- .../java/test/managers/ConfigManager.java | 3 + .../java/test/utils/McMMOIntegration.java | 261 ++++++++++++++++++ src/main/resources/paper-plugin.yml | 3 +- src/main/resources/plugin.yml | 3 +- 7 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 src/main/java/test/utils/McMMOIntegration.java diff --git a/README.md b/README.md index c410c4e..66c48ee 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ simple plugin to right click harvest crops - Replants automatically - Supports foila and paper! Mostly tested! - Can be configured with the json (check below) +- mcmmo integration ## configuration @@ -17,7 +18,8 @@ The plugin generates a `harvester.json` file in its data folder: - `Use_Durability`: (true/false) Whether the hoe should lose durability. - `DropTo`: ('ground' or 'inventory') Where to put harvested items. - `Require_Hoe`: (true/false) Whether a hoe is required to harvest. -- **Inventory Full Notification**: If `DropTo` is set to `inventory` and the player's inventory is full, an action bar message will appear and items will drop to the ground. +- `Inventory Full Notification`: If `DropTo` is set to `inventory` and the player's inventory is full, an action bar message will appear and items will drop to the ground. +- `McMMO_Integration`: (true/false): enables integration with McMMO. ## supported Crops diff --git a/pom.xml b/pom.xml index 62c7d69..ecb7a5d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,10 +6,10 @@ test harvester - 1.0.0-SNAPSHOT + 1.1 Right Click Harvester - https://example.com + https://coldfiles.dev UTF-8 @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/src/main/java/test/listeners/CropHarvestListener.java b/src/main/java/test/listeners/CropHarvestListener.java index 7974f5a..98ec311 100644 --- a/src/main/java/test/listeners/CropHarvestListener.java +++ b/src/main/java/test/listeners/CropHarvestListener.java @@ -14,6 +14,7 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import test.managers.ConfigManager; +import test.utils.McMMOIntegration; import java.util.Collection; import java.util.Map; @@ -54,6 +55,16 @@ public class CropHarvestListener implements Listener { Collection drops = block.getDrops(itemInHand); Material seedType = getSeedType(block.getType()); + // Check for mcMMO Double Drops / Verdant Bounty before processing drops + boolean doubleDrops = false; + boolean tripleDrops = false; + if (config.McMMO_Integration && McMMOIntegration.tryEnable()) { + doubleDrops = McMMOIntegration.checkDoubleDrops(player); + if (doubleDrops) { + tripleDrops = McMMOIntegration.checkVerdantBounty(player); + } + } + for (ItemStack drop : drops) { // If Drop_Seeds is false, skip seeds if (!config.Drop_Seeds && drop.getType() == seedType) { @@ -72,6 +83,41 @@ public class CropHarvestListener implements Listener { } else { block.getWorld().dropItemNaturally(block.getLocation(), drop); } + + // mcMMO Double Drops: drop an extra copy of the item + if (doubleDrops && drop.getType() != seedType) { + ItemStack bonus = drop.clone(); + if (config.DropTo.equalsIgnoreCase("inventory")) { + Map leftover = player.getInventory().addItem(bonus); + if (!leftover.isEmpty()) { + for (ItemStack item : leftover.values()) { + block.getWorld().dropItemNaturally(block.getLocation(), item); + } + } + } else { + block.getWorld().dropItemNaturally(block.getLocation(), bonus); + } + + // Triple Drops: drop a second extra copy + if (tripleDrops) { + ItemStack triple = drop.clone(); + if (config.DropTo.equalsIgnoreCase("inventory")) { + Map leftover = player.getInventory().addItem(triple); + if (!leftover.isEmpty()) { + for (ItemStack item : leftover.values()) { + block.getWorld().dropItemNaturally(block.getLocation(), item); + } + } + } else { + block.getWorld().dropItemNaturally(block.getLocation(), triple); + } + } + } + } + + // Spawn particles if mcMMO skills procced + if (doubleDrops) { + McMMOIntegration.spawnSkillParticles(block.getLocation(), tripleDrops); } // Reset age to 0 (freshly planted) @@ -88,7 +134,19 @@ public class CropHarvestListener implements Listener { } } } - + + // Award mcMMO Herbalism XP from mcMMO's own experience config + if (config.McMMO_Integration) { + int xpAmount = McMMOIntegration.getHerbalismXpFor(block.getType()); + if (xpAmount > 0) { + int result = McMMOIntegration.addHerbalismXp(player, xpAmount); + if (result < 0) { + player.getServer().getLogger().warning("[Harvester] McMMO errored or not installed, Disabling McMMO integration..."); + config.McMMO_Integration = false; + } + } + } + event.setCancelled(true); } diff --git a/src/main/java/test/managers/ConfigManager.java b/src/main/java/test/managers/ConfigManager.java index 3915dce..28437c9 100644 --- a/src/main/java/test/managers/ConfigManager.java +++ b/src/main/java/test/managers/ConfigManager.java @@ -73,5 +73,8 @@ public class ConfigManager { public String _comment4 = "Require_Hoe: Whether a hoe is required to harvest (true/false)"; public boolean Require_Hoe = true; + + public String _comment5 = "McMMO_Integration: Enable mcMMO Herbalism XP and passive skills (true/false)"; + public boolean McMMO_Integration = true; } } diff --git a/src/main/java/test/utils/McMMOIntegration.java b/src/main/java/test/utils/McMMOIntegration.java new file mode 100644 index 0000000..e5a929e --- /dev/null +++ b/src/main/java/test/utils/McMMOIntegration.java @@ -0,0 +1,261 @@ +package test.utils; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.entity.Player; + +import java.lang.reflect.Method; +import java.util.concurrent.ThreadLocalRandom; +import java.util.logging.Level; + +/** + * Reflection-based mcMMO integration. No compile-time dependency required. + * If mcMMO is not present at runtime, all calls are safely no-ops. + *

+ * Uses mcMMO's own XP values from experience.yml, checks for Double Drops + * and Verdant Bounty passive skills, and spawns particles on proc. + */ +public class McMMOIntegration { + + private static boolean enabled = false; + private static boolean checked = false; + + // Cached reflection methods + private static Method addRawXpMethod; + private static Method getUserManagerGetPlayer; + private static Method getExperienceConfigInstance; + private static Method getXpMethod; + private static Method hasUnlockedSubskill; + private static Method isSubSkillEnabled; + private static Method getPlayerLevel; + private static Object experienceConfigInstance; + + // Cached enum values + private static Object primarySkillHerbalism; + private static Object subSkillDoubleDrops; + private static Object subSkillVerdantBounty; + + /** + * Attempt to find and cache mcMMO API methods. + * Uses the mcMMO plugin's classloader to bypass classloader isolation. + */ + public static boolean tryEnable() { + if (checked) { + return enabled; + } + checked = true; + + try { + ClassLoader mcmmoLoader = getMcMMOClassLoader(); + if (mcmmoLoader == null) { + return false; + } + + // Cache PrimarySkillType.HERBALISM + Class primarySkillType = Class.forName("com.gmail.nossr50.datatypes.skills.PrimarySkillType", true, mcmmoLoader); + primarySkillHerbalism = Enum.valueOf((Class) primarySkillType, "HERBALISM"); + + // Cache SubSkillType values + Class subSkillType = Class.forName("com.gmail.nossr50.datatypes.skills.SubSkillType", true, mcmmoLoader); + subSkillDoubleDrops = Enum.valueOf((Class) subSkillType, "HERBALISM_DOUBLE_DROPS"); + subSkillVerdantBounty = Enum.valueOf((Class) subSkillType, "HERBALISM_VERDANT_BOUNTY"); + + // Cache ExperienceAPI.addRawXP(Player, String, float, String, boolean) + Class experienceAPI = Class.forName("com.gmail.nossr50.api.ExperienceAPI", true, mcmmoLoader); + addRawXpMethod = experienceAPI.getMethod("addRawXP", Player.class, String.class, float.class, String.class, boolean.class); + + // Cache UserManager.getPlayer(Player) + Class userManager = Class.forName("com.gmail.nossr50.util.player.UserManager", true, mcmmoLoader); + getUserManagerGetPlayer = userManager.getMethod("getPlayer", Player.class); + + // Cache ExperienceConfig.getInstance() and getXp(PrimarySkillType, Material) + Class experienceConfig = Class.forName("com.gmail.nossr50.config.experience.ExperienceConfig", true, mcmmoLoader); + getExperienceConfigInstance = experienceConfig.getMethod("getInstance"); + getXpMethod = experienceConfig.getMethod("getXp", primarySkillType, Material.class); + experienceConfigInstance = getExperienceConfigInstance.invoke(null); + + // Cache RankUtils.hasUnlockedSubskill(Player, SubSkillType) + Class rankUtils = Class.forName("com.gmail.nossr50.util.skills.RankUtils", true, mcmmoLoader); + hasUnlockedSubskill = rankUtils.getMethod("hasUnlockedSubskill", Player.class, subSkillType); + + // Cache Permissions.isSubSkillEnabled(Permissible, SubSkillType) + Class permissions = Class.forName("com.gmail.nossr50.util.Permissions", true, mcmmoLoader); + Class permissible = Class.forName("org.bukkit.permissions.Permissible", true, mcmmoLoader); + isSubSkillEnabled = permissions.getMethod("isSubSkillEnabled", permissible, subSkillType); + + // Cache ExperienceAPI.getLevel(Player, PrimarySkillType) + getPlayerLevel = experienceAPI.getMethod("getLevel", Player.class, primarySkillType); + + enabled = true; + Bukkit.getLogger().info("[Harvester] mcMMO integration enabled!"); + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "[Harvester] Failed to hook mcMMO API: " + e.getMessage()); + enabled = false; + } + + return enabled; + } + + /** + * Check if a player has their mcMMO data loaded. + */ + private static boolean isPlayerDataLoaded(Player player) { + if (!tryEnable()) { + return false; + } + try { + Object mmoPlayer = getUserManagerGetPlayer.invoke(null, player); + return mmoPlayer != null; + } catch (Exception e) { + return false; + } + } + + /** + * Get the mcMMO-configured XP value for a given crop material. + * + * @param cropMaterial the block material to look up + * @return the XP value from mcMMO's experience.yml, or 0 if unavailable + */ + public static int getHerbalismXpFor(Material cropMaterial) { + if (!tryEnable()) { + return 0; + } + try { + return (int) getXpMethod.invoke(experienceConfigInstance, primarySkillHerbalism, cropMaterial); + } catch (Exception e) { + return 0; + } + } + + /** + * Check if the player's Double Drops passive should proc. + * Uses mcMMO's own rank/permission checks and probability scaling. + * + * @param player the player to check + * @return true if double drops should activate + */ + public static boolean checkDoubleDrops(Player player) { + if (!tryEnable() || !isPlayerDataLoaded(player)) { + return false; + } + try { + // Check if the subskill is unlocked and enabled + boolean unlocked = (boolean) hasUnlockedSubskill.invoke(null, player, subSkillDoubleDrops); + boolean enabled_ = (boolean) isSubSkillEnabled.invoke(null, player, subSkillDoubleDrops); + if (!unlocked || !enabled_) { + return false; + } + + // Get player level and calculate chance (0% at 0, 100% at 1000) + int level = (int) getPlayerLevel.invoke(null, player, primarySkillHerbalism); + double chance = Math.min(100.0, (level / 1000.0) * 100.0); + return ThreadLocalRandom.current().nextDouble(100.0) < chance; + } catch (Exception e) { + return false; + } + } + + /** + * Check if the player's Verdant Bounty passive should proc (triple drops). + * Only rolls if double drops already procced. + * Scales from 0% to 50% at level 10000. + * + * @param player the player to check + * @return true if verdant bounty should activate + */ + public static boolean checkVerdantBounty(Player player) { + if (!tryEnable() || !isPlayerDataLoaded(player)) { + return false; + } + try { + // Check if the subskill is unlocked and enabled + boolean unlocked = (boolean) hasUnlockedSubskill.invoke(null, player, subSkillVerdantBounty); + boolean enabled_ = (boolean) isSubSkillEnabled.invoke(null, player, subSkillVerdantBounty); + if (!unlocked || !enabled_) { + return false; + } + + // Get player level and calculate chance (0% at 0, 50% at 10000) + int level = (int) getPlayerLevel.invoke(null, player, primarySkillHerbalism); + double chance = Math.min(50.0, (level / 10000.0) * 50.0); + return ThreadLocalRandom.current().nextDouble(100.0) < chance; + } catch (Exception e) { + return false; + } + } + + /** + * Award Herbalism XP to a player using mcMMO's own XP pipeline. + * + * @param player the player to award XP to + * @param amount the amount of XP to award + * @return 1 if XP was awarded, 0 if player data not loaded yet (retry later), -1 on error (disable) + */ + public static int addHerbalismXp(Player player, int amount) { + if (!tryEnable()) { + return -1; + } + + if (!isPlayerDataLoaded(player)) { + return 0; + } + + try { + addRawXpMethod.invoke(null, player, "HERBALISM", (float) amount, "PVE", false); + return 1; + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "[Harvester] mcMMO XP award failed: " + e.getMessage()); + enabled = false; + return -1; + } + } + + /** + * Spawn particles at a block location to indicate skill procs. + * + * @param location the block location + * @param isTriple true for triple drop particles, false for double drop + */ + public static void spawnSkillParticles(Location location, boolean isTriple) { + if (location == null || location.getWorld() == null) { + return; + } + Location center = location.clone().add(0.5, 0.5, 0.5); + + if (isTriple) { + // Triple drops: bigger particle burst + location.getWorld().spawnParticle( + Particle.HAPPY_VILLAGER, + center, 20, 0.4, 0.4, 0.4, 0.1 + ); + location.getWorld().spawnParticle( + Particle.HEART, + center, 8, 0.3, 0.3, 0.3, 0.01 + ); + } else { + // Double drops: smaller particle burst + location.getWorld().spawnParticle( + Particle.HAPPY_VILLAGER, + center, 10, 0.3, 0.3, 0.3, 0.05 + ); + } + } + + private static ClassLoader getMcMMOClassLoader() { + org.bukkit.plugin.Plugin mcmmo = Bukkit.getPluginManager().getPlugin("mcMMO"); + if (mcmmo == null || !mcmmo.isEnabled()) { + return null; + } + return mcmmo.getClass().getClassLoader(); + } + + /** + * Whether mcMMO integration is currently active. + */ + public static boolean isEnabled() { + return enabled; + } +} diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index 96cb546..ceaaf8c 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,6 +1,7 @@ name: Harvester -version: 1.0.0-SNAPSHOT +version: 1.1 main: test.Harvester api-version: '1.21' author: test folia-supported: true +softdepend: [mcMMO] diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d033019..48f7095 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,6 @@ main: test.Harvester -version: 1.0.0-SNAPSHOT +version: 1.1 name: Harvester author: test api-version: 1.21 +softdepend: [mcMMO]