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:
parent
5802de069b
commit
40faf76928
7 changed files with 333 additions and 7 deletions
|
|
@ -8,6 +8,7 @@ simple plugin to right click harvest crops
|
||||||
- Replants automatically
|
- Replants automatically
|
||||||
- Supports foila and paper! Mostly tested!
|
- Supports foila and paper! Mostly tested!
|
||||||
- Can be configured with the json (check below)
|
- Can be configured with the json (check below)
|
||||||
|
- mcmmo integration
|
||||||
|
|
||||||
## configuration
|
## 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.
|
- `Use_Durability`: (true/false) Whether the hoe should lose durability.
|
||||||
- `DropTo`: ('ground' or 'inventory') Where to put harvested items.
|
- `DropTo`: ('ground' or 'inventory') Where to put harvested items.
|
||||||
- `Require_Hoe`: (true/false) Whether a hoe is required to harvest.
|
- `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
|
## supported Crops
|
||||||
|
|
||||||
|
|
|
||||||
6
pom.xml
6
pom.xml
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
<groupId>test</groupId>
|
<groupId>test</groupId>
|
||||||
<artifactId>harvester</artifactId>
|
<artifactId>harvester</artifactId>
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
<version>1.1</version>
|
||||||
|
|
||||||
<name>Right Click Harvester</name>
|
<name>Right Click Harvester</name>
|
||||||
<url>https://example.com</url>
|
<url>https://coldfiles.dev</url>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
@ -53,4 +53,4 @@
|
||||||
<plugins>
|
<plugins>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.Damageable;
|
import org.bukkit.inventory.meta.Damageable;
|
||||||
import test.managers.ConfigManager;
|
import test.managers.ConfigManager;
|
||||||
|
import test.utils.McMMOIntegration;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -54,6 +55,16 @@ public class CropHarvestListener implements Listener {
|
||||||
Collection<ItemStack> drops = block.getDrops(itemInHand);
|
Collection<ItemStack> drops = block.getDrops(itemInHand);
|
||||||
Material seedType = getSeedType(block.getType());
|
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) {
|
for (ItemStack drop : drops) {
|
||||||
// If Drop_Seeds is false, skip seeds
|
// If Drop_Seeds is false, skip seeds
|
||||||
if (!config.Drop_Seeds && drop.getType() == seedType) {
|
if (!config.Drop_Seeds && drop.getType() == seedType) {
|
||||||
|
|
@ -72,6 +83,41 @@ public class CropHarvestListener implements Listener {
|
||||||
} else {
|
} else {
|
||||||
block.getWorld().dropItemNaturally(block.getLocation(), drop);
|
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)
|
// 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);
|
event.setCancelled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,5 +73,8 @@ public class ConfigManager {
|
||||||
|
|
||||||
public String _comment4 = "Require_Hoe: Whether a hoe is required to harvest (true/false)";
|
public String _comment4 = "Require_Hoe: Whether a hoe is required to harvest (true/false)";
|
||||||
public boolean Require_Hoe = true;
|
public boolean Require_Hoe = true;
|
||||||
|
|
||||||
|
public String _comment5 = "McMMO_Integration: Enable mcMMO Herbalism XP and passive skills (true/false)";
|
||||||
|
public boolean McMMO_Integration = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
261
src/main/java/test/utils/McMMOIntegration.java
Normal file
261
src/main/java/test/utils/McMMOIntegration.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
name: Harvester
|
name: Harvester
|
||||||
version: 1.0.0-SNAPSHOT
|
version: 1.1
|
||||||
main: test.Harvester
|
main: test.Harvester
|
||||||
api-version: '1.21'
|
api-version: '1.21'
|
||||||
author: test
|
author: test
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
|
softdepend: [mcMMO]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
main: test.Harvester
|
main: test.Harvester
|
||||||
version: 1.0.0-SNAPSHOT
|
version: 1.1
|
||||||
name: Harvester
|
name: Harvester
|
||||||
author: test
|
author: test
|
||||||
api-version: 1.21
|
api-version: 1.21
|
||||||
|
softdepend: [mcMMO]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue