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]