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
This commit is contained in:
Professor Doom 2026-06-21 17:14:36 -05:00
parent 5802de069b
commit 40faf76928
7 changed files with 333 additions and 7 deletions

View file

@ -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

View file

@ -6,10 +6,10 @@
<groupId>test</groupId>
<artifactId>harvester</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1</version>
<name>Right Click Harvester</name>
<url>https://example.com</url>
<url>https://coldfiles.dev</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -53,4 +53,4 @@
<plugins>
</plugins>
</build>
</project>
</project>

View file

@ -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<ItemStack> 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<Integer, ItemStack> 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<Integer, ItemStack> 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);
}

View file

@ -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;
}
}

View file

@ -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.
* <p>
* 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<Enum>) primarySkillType, "HERBALISM");
// Cache SubSkillType values
Class<?> subSkillType = Class.forName("com.gmail.nossr50.datatypes.skills.SubSkillType", true, mcmmoLoader);
subSkillDoubleDrops = Enum.valueOf((Class<Enum>) subSkillType, "HERBALISM_DOUBLE_DROPS");
subSkillVerdantBounty = Enum.valueOf((Class<Enum>) 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;
}
}

View file

@ -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]

View file

@ -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]