Просмотр исходного кода

merge configurable into api, don't hurt me

nossr50 5 лет назад
Родитель
Сommit
1204b9a94c
65 измененных файлов с 2099 добавлено и 217 удалено
  1. 1 0
      .gitattributes
  2. 0 11
      1
  3. 17 0
      Changelog.txt
  4. 3 0
      build.gradle.kts
  5. 0 1
      mcmmo-api/src/main/java/com/gmail/nossr50/mcmmo/api/platform/util/MetadataStore.java
  6. 1 7
      mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/BukkitBoostrap.java
  7. 0 1
      mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/platform/scheduler/BukkitPlatformScheduler.java
  8. 0 1
      mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/platform/scheduler/MMOBukkitTask.java
  9. 0 1
      mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/platform/util/BukkitMobHealthBarManager.java
  10. 8 6
      mcmmo-bukkit/src/main/resources/plugin.yml
  11. 28 4
      mcmmo-core/build.gradle.kts
  12. 66 0
      mcmmo-core/src/main/java/com/gmail/nossr50/commands/admin/NBTToolsCommand.java
  13. 0 20
      mcmmo-core/src/main/java/com/gmail/nossr50/commands/admin/PlayerDebug.java
  14. 14 11
      mcmmo-core/src/main/java/com/gmail/nossr50/commands/admin/PlayerDebugCommand.java
  15. 0 1
      mcmmo-core/src/main/java/com/gmail/nossr50/config/skills/ranks/SkillRankProperty.java
  16. 1 0
      mcmmo-core/src/main/java/com/gmail/nossr50/config/sound/ConfigSound.java
  17. 3 1
      mcmmo-core/src/main/java/com/gmail/nossr50/core/MetadataConstants.java
  18. 34 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/PlatformManager.java
  19. 13 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/NBTAdapter.java
  20. 158 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/NMS_114/BukkitNBTAdapter.java
  21. 11 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/NMS_114/BukkitPlatformAdapter.java
  22. 19 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/PlatformAdapter.java
  23. 11 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTBase.java
  24. 45 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTByte.java
  25. 49 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTByteArray.java
  26. 65 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTCompound.java
  27. 45 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTDouble.java
  28. 8 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTEnd.java
  29. 45 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTFloat.java
  30. 45 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTInt.java
  31. 49 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTIntArray.java
  32. 53 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTList.java
  33. 45 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTLong.java
  34. 49 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTLongArray.java
  35. 45 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTShort.java
  36. 50 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTString.java
  37. 22 0
      mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTType.java
  38. 1 0
      mcmmo-core/src/main/java/com/gmail/nossr50/datatypes/meta/OldName.java
  39. 17 0
      mcmmo-core/src/main/java/com/gmail/nossr50/datatypes/meta/RecentlyReplantedCropMeta.java
  40. 11 19
      mcmmo-core/src/main/java/com/gmail/nossr50/datatypes/skills/behaviours/HerbalismBehaviour.java
  41. 1 1
      mcmmo-core/src/main/java/com/gmail/nossr50/dumpster/HolidayManager.java
  42. 330 0
      mcmmo-core/src/main/java/com/gmail/nossr50/dumpster/SalvageManager.java
  43. 12 15
      mcmmo-core/src/main/java/com/gmail/nossr50/listeners/BlockListener.java
  44. 8 5
      mcmmo-core/src/main/java/com/gmail/nossr50/listeners/EntityListener.java
  45. 14 0
      mcmmo-core/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  46. 1 1
      mcmmo-core/src/main/java/com/gmail/nossr50/locale/LocaleManager.java
  47. 24 9
      mcmmo-core/src/main/java/com/gmail/nossr50/mcMMO.java
  48. 1 2
      mcmmo-core/src/main/java/com/gmail/nossr50/runnables/skills/BleedTimerTask.java
  49. 100 0
      mcmmo-core/src/main/java/com/gmail/nossr50/runnables/skills/DelayedCropReplant.java
  50. 1 2
      mcmmo-core/src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java
  51. 1 2
      mcmmo-core/src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java
  52. 118 40
      mcmmo-core/src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java
  53. 3 4
      mcmmo-core/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java
  54. 1 2
      mcmmo-core/src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java
  55. 105 0
      mcmmo-core/src/main/java/com/gmail/nossr50/text/TextManager.java
  56. 59 11
      mcmmo-core/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
  57. 71 0
      mcmmo-core/src/main/java/com/gmail/nossr50/util/nbt/NBTFactory.java
  58. 167 17
      mcmmo-core/src/main/java/com/gmail/nossr50/util/nbt/NBTManager.java
  59. 9 2
      mcmmo-core/src/main/java/com/gmail/nossr50/util/player/UserManager.java
  60. 7 8
      mcmmo-core/src/main/java/com/gmail/nossr50/util/skills/CombatTools.java
  61. 21 11
      mcmmo-core/src/main/java/com/gmail/nossr50/util/skills/ParticleEffectUtils.java
  62. 7 0
      mcmmo-core/src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java
  63. 1 0
      mcmmo-core/src/main/java/com/gmail/nossr50/util/sounds/SoundType.java
  64. 1 1
      mcmmo-core/src/main/resources/com/gmail/nossr50/locale/locale_en_US.properties
  65. 4 0
      mcmmo-core/src/main/resources/sounds.yml

+ 1 - 0
.gitattributes

@@ -2,3 +2,4 @@
 
 *.png binary
 *.wav binary
+p

+ 0 - 11
1

@@ -1,11 +0,0 @@
-SkillShot tweaks
-# Please enter the commit message for your changes. Lines starting
-# with '#' will be ignored, and an empty message aborts the commit.
-#
-# On branch master
-# Your branch is up to date with 'origin/master'.
-#
-# Changes to be committed:
-#	modified:   src/main/java/com/gmail/nossr50/skills/archery/Archery.java
-#	modified:   src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java
-#

+ 17 - 0
Changelog.txt

@@ -203,6 +203,23 @@ Version 2.2.0
     Added API method to check if a skill was being level capped
     Added 'UndefinedSkillBehaviour' for trying to use a method that has no behaviour defined for the provided skill
 
+Version 2.1.115
+    Green Thumb now requires a hoe to activate
+    Hoes no longer give free replants
+    You can sneak to break plants with a hoe in your hand (or just put the hoe away)
+    Using a hoe on non-fully grown crops will replant them as a convenience feature
+    New sound option in sounds.yml called 'ITEM_CONSUMED', plays when eating seeds for Green Thumb
+    Cocoa plants now require GT of at least 2 to start at the second stage of growth
+    Green Terra now boosts growth on Green Thumb by 1 stage (doesn't go above the maximum value though)
+    There is now a feature in place to prevent breaking a newly automatically replanted (via green thumb) crop from being breakable for a few seconds after it appears
+    Fixed a bug where Salvage always gave the best results
+    Fixed an issue with arrows causing exceptions with players not yet having data loaded
+    Spectral arrows are now tracked by mcMMO
+    Use minimum level of salvageable properly
+    Fix Axes Critical Strikes default permissions ( new fixed permission: mcmmo.ability.axes.criticalstrikes )
+    Fix potential null pointer exception for salvage
+    Updated locale entry 'Herbalism.SubSkill.GreenTerra.Description'
+
 Version 2.1.114
     Fix some more locale usages, should aim to further prevent issues with oddball locales
     Fixed a bug where newer versions of MySQL did not like our rank command

+ 3 - 0
build.gradle.kts

@@ -6,11 +6,14 @@ subprojects {
     repositories {
         mavenLocal()
         mavenCentral()
+        maven("https://oss.sonatype.org/content/groups/public/")
         maven("https://repo.spongepowered.org/maven")
         maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots")
         maven("https://repo.codemc.org/repository/maven-public")
         maven("https://maven.sk89q.com/repo")
         maven("https://mvnrepository.com/artifact/org.jetbrains/annotations")
+        maven("https://repo.aikar.co/content/groups/aikar/")
+        maven("https://hub.spigotmc.org/nexus/content/groups/public/")
     }
 
     tasks {

+ 0 - 1
mcmmo-api/src/main/java/com/gmail/nossr50/mcmmo/api/platform/util/MetadataStore.java

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.mcmmo.api.platform.util;
 
 import com.gmail.nossr50.mcmmo.api.data.MMOEntity;
-
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 

+ 1 - 7
mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/BukkitBoostrap.java

@@ -1,11 +1,6 @@
 package com.gmail.nossr50.mcmmo.bukkit;
 
-import com.gmail.nossr50.listeners.BlockListener;
-import com.gmail.nossr50.listeners.EntityListener;
-import com.gmail.nossr50.listeners.InventoryListener;
-import com.gmail.nossr50.listeners.PlayerListener;
-import com.gmail.nossr50.listeners.SelfListener;
-import com.gmail.nossr50.listeners.WorldListener;
+import com.gmail.nossr50.listeners.*;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcmmo.api.platform.PlatformProvider;
 import com.gmail.nossr50.mcmmo.api.platform.ServerSoftwareType;
@@ -14,7 +9,6 @@ import com.gmail.nossr50.mcmmo.api.platform.util.MetadataStore;
 import com.gmail.nossr50.mcmmo.api.platform.util.MobHealthBarManager;
 import com.gmail.nossr50.mcmmo.bukkit.platform.scheduler.BukkitPlatformScheduler;
 import com.gmail.nossr50.mcmmo.bukkit.platform.util.BukkitMobHealthBarManager;
-
 import org.bstats.bukkit.Metrics;
 import org.bukkit.Bukkit;
 import org.bukkit.event.HandlerList;

+ 0 - 1
mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/platform/scheduler/BukkitPlatformScheduler.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.mcmmo.bukkit.platform.scheduler;
 import com.gmail.nossr50.mcmmo.api.platform.scheduler.PlatformScheduler;
 import com.gmail.nossr50.mcmmo.api.platform.scheduler.Task;
 import com.gmail.nossr50.mcmmo.bukkit.BukkitBoostrap;
-
 import org.bukkit.Bukkit;
 import org.bukkit.scheduler.BukkitScheduler;
 import org.bukkit.scheduler.BukkitTask;

+ 0 - 1
mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/platform/scheduler/MMOBukkitTask.java

@@ -2,7 +2,6 @@ package com.gmail.nossr50.mcmmo.bukkit.platform.scheduler;
 
 import com.gmail.nossr50.mcmmo.api.platform.scheduler.Task;
 import com.google.common.base.Preconditions;
-
 import org.bukkit.scheduler.BukkitTask;
 
 import java.util.function.Consumer;

+ 0 - 1
mcmmo-bukkit/src/main/java/com/gmail/nossr50/mcmmo/bukkit/platform/util/BukkitMobHealthBarManager.java

@@ -9,7 +9,6 @@ import com.gmail.nossr50.mcmmo.api.data.MMOPlayer;
 import com.gmail.nossr50.mcmmo.api.platform.util.MobHealthBarManager;
 import com.gmail.nossr50.runnables.MobHealthDisplayUpdaterTask;
 import com.gmail.nossr50.util.StringUtils;
-
 import org.bukkit.Bukkit;
 import org.bukkit.ChatColor;
 import org.bukkit.entity.LivingEntity;

+ 8 - 6
mcmmo-bukkit/src/main/resources/plugin.yml

@@ -19,9 +19,6 @@ load: POSTWORLD
 api-version: 1.13
 
 commands:
-    mmodebug:
-        aliases: [mcmmodebugmode]
-        description: Toggles a debug mode which will print useful information to chat
     mmoinfo:
         aliases: [mcinfo]
         description: Info pages for mcMMO
@@ -264,7 +261,7 @@ permissions:
         description: Allows access to all Axes abilities
         children:
             mcmmo.ability.axes.axemastery: true
-            mcmmo.ability.axes.criticalhit: true
+            mcmmo.ability.axes.criticalstrikes: true
             mcmmo.ability.axes.greaterimpact: true
             mcmmo.ability.axes.armorimpact: true
             mcmmo.ability.axes.skullsplitter: true
@@ -273,8 +270,8 @@ permissions:
         description: Adds damage to axes
     mcmmo.ability.axes.axemastery:
         description: Allows bonus damage from Axes
-    mcmmo.ability.axes.criticalhit:
-        description: Allows access to the Critical Hit ability
+    mcmmo.ability.axes.criticalstrikes:
+        description: Allows access to the Critical Strikes ability
     mcmmo.ability.axes.greaterimpact:
         description: Allows access to the Greater Impact ability
     mcmmo.ability.axes.armorimpact:
@@ -628,6 +625,10 @@ permissions:
         children:
             mcmmo.commands.mcconvert.all: true
             mcmmo.commands.xprate.all: true
+            mcmmo.commands.nbttools: true
+    mcmmo.commands.nbttools:
+        default: false
+        description: Modify or Read NBT of an item in-hand
     mcmmo.bypass.*:
         default: false
         description: Implies all bypass permissions.
@@ -731,6 +732,7 @@ permissions:
             mcmmo.commands.mcmmoreload: true
             mcmmo.commands.mmoedit: true
             mcmmo.commands.mmoedit.others: true
+            mcmmo.commands.nbttools: true
             mcmmo.commands.mmoshowdb: true
             mcmmo.commands.ptp.world.all: true
             mcmmo.commands.reloadlocale: true

+ 28 - 4
mcmmo-core/build.gradle.kts

@@ -1,3 +1,4 @@
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
 import org.apache.tools.ant.filters.ReplaceTokens
 
 plugins {
@@ -7,10 +8,6 @@ plugins {
 
 tasks {
 
-    build {
-        dependsOn(shadowJar)
-    }
-
     shadowJar {
         dependencies {
             include(dependency("org.spongepowered:configurate-yaml"))
@@ -20,12 +17,24 @@ tasks {
             include(dependency("org.apache.tomcat:tomcat-jdbc"))
             include(dependency("org.apache.tomcat:tomcat-juli"))
             include(dependency("com.typesafe:config"))
+            include(dependency("co.aikar:acf-core"))
+            include(dependency("co.aikar:acf-bukkit"))
+            include(dependency("net.kyori:text-api"))
+            include(dependency("net.kyori:text-adapter-bukkit"))
+            include(dependency("net.kyori:text-serializer-gson"))
             exclude(dependency("org.spigotmc:spigot"))
         }
         relocate("org.apache.commons.logging", "com.gmail.nossr50.commons.logging")
         relocate("org.apache.juli", "com.gmail.nossr50.database.tomcat.juli")
         relocate("org.apache.tomcat", "com.gmail.nossr50.database.tomcat")
         relocate("org.bstats", "com.gmail.nossr50.metrics.bstat")
+        relocate("co.aikar.commands", "com.gmail.nossr50.aikar.commands")
+        relocate("co.aikar.locales", "com.gmail.nossr50.aikar.locales")
+        relocate("co.aikar.table", "com.gmail.nossr50.aikar.table")
+        relocate("net.jodah.expiringmap", "com.gmail.nossr50.expiringmap")
+        relocate("net.kyori.text", "com.gmail.nossr50.kyoripowered.text")
+
+        mergeServiceFiles()
     }
 
     processResources {
@@ -34,6 +43,16 @@ tasks {
 
         }
     }
+
+    build {
+        dependsOn(shadowJar)
+    }
+}
+
+tasks.named<ShadowJar>("shadowJar") {
+    dependencies{
+        include { true }
+    }
 }
 
 
@@ -44,6 +63,11 @@ dependencies {
     api("org.spongepowered:configurate-core:3.7-SNAPSHOT")
     api("org.spongepowered:configurate-yaml:3.7-SNAPSHOT")
     api("org.spongepowered:configurate-hocon:3.7-SNAPSHOT")
+    api("co.aikar:acf-core:0.5.0-SNAPSHOT") //Don't change without updating the artifacts for its dependencies (see the other comments)
+    api("co.aikar:acf-paper:0.5.0-SNAPSHOT") //Don't change without updating the artifacts for its dependencies (see the other comments)
+    api("net.kyori:text-api:3.0.2")
+    api("net.kyori:text-serializer-gson:3.0.2")
+    api("net.kyori:text-adapter-bukkit:3.0.4-SNAPSHOT")
     implementation("org.jetbrains:annotations:17.0.0")
     implementation("org.apache.maven.scm:maven-scm-provider-gitexe:1.8.1")
     implementation("org.bstats:bstats-bukkit:1.4")

+ 66 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/commands/admin/NBTToolsCommand.java

@@ -0,0 +1,66 @@
+package com.gmail.nossr50.commands.admin;
+
+import co.aikar.commands.BaseCommand;
+import co.aikar.commands.annotation.*;
+import com.gmail.nossr50.mcMMO;
+import net.kyori.text.TextComponent;
+import net.kyori.text.adapter.bukkit.TextAdapter;
+import net.kyori.text.format.TextColor;
+import net.kyori.text.serializer.gson.GsonComponentSerializer;
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+
+@CommandAlias("nbttools")
+@Description("Read or Modify values of NBT on an item in-hand")
+public class NBTToolsCommand extends BaseCommand {
+
+    public static final String STYLE_TEXT_1 = "//////////";
+    @Dependency
+    private mcMMO plugin;
+
+    @Default
+    @CommandPermission("mcmmo.commands.nbttools")
+    public void onCommand(Player player) {
+        //TODO: Add some help messages
+        player.sendMessage("hi");
+    }
+
+    /**
+     * Show the NBT tags of an item in hand
+     */
+    @Subcommand("tags show")
+    public void onShowTags(Player player) {
+        final TextComponent textComponent = TextComponent.builder()
+                .content(plugin.getLocaleManager().getString("mcMMO.Template.Prefix"))
+                .append("NBT Tools")
+                .color(TextColor.GOLD)
+                .append(" - ")
+                .append("Showing NBT Tags (")
+                .append(player.getInventory().getItemInMainHand().getType().getKey().toString())
+                .color(TextColor.GREEN)
+                .append(")")
+                .color(TextColor.GOLD)
+                .append(TextComponent.newline())
+                .build();
+
+        String json = GsonComponentSerializer.INSTANCE.serialize(textComponent);
+        TextAdapter.sendMessage(player, textComponent);
+
+        //Show NBT tags to player
+        player.sendMessage(STYLE_TEXT_1 + " NBT TOOLS " + STYLE_TEXT_1);
+        player.sendMessage("NBT Analysis: " + player.getInventory().getItemInMainHand().getType().getKey().toString());
+        player.sendMessage(STYLE_TEXT_1 + STYLE_TEXT_1);
+        plugin.getNbtManager().printNBT(player.getInventory().getItemInMainHand(), player);
+        player.sendMessage(ChatColor.GRAY + "NBT Analysis completed!");
+    }
+
+    @Subcommand("tags add")
+    public void onAddTags(Player player) {
+
+    }
+
+    @Subcommand("tags remove")
+    public void onRemoveTags(Player player) {
+
+    }
+}

+ 0 - 20
mcmmo-core/src/main/java/com/gmail/nossr50/commands/admin/PlayerDebug.java

@@ -1,20 +0,0 @@
-package com.gmail.nossr50.commands.admin;
-
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
-import org.bukkit.command.CommandSender;
-
-public class PlayerDebug implements CommandExecutor {
-
-    private final mcMMO pluginRef;
-
-    public PlayerDebug(mcMMO pluginRef) {
-        this.pluginRef = pluginRef;
-    }
-
-    @Override
-    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
-        return false;
-    }
-}

+ 14 - 11
mcmmo-core/src/main/java/com/gmail/nossr50/commands/admin/PlayerDebugCommand.java

@@ -1,29 +1,32 @@
 package com.gmail.nossr50.commands.admin;
 
+import co.aikar.commands.BaseCommand;
+import co.aikar.commands.annotation.CommandAlias;
+import co.aikar.commands.annotation.Default;
+import co.aikar.commands.annotation.Dependency;
+import co.aikar.commands.annotation.Description;
 import com.gmail.nossr50.datatypes.player.BukkitMMOPlayer;
 import com.gmail.nossr50.mcMMO;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
 
-public class PlayerDebugCommand implements CommandExecutor {
 
-    private final mcMMO pluginRef;
+@CommandAlias("mmodebug")
+@Description("Puts the player into debug mode, which helps problem solve bugs in mcMMO.")
+public class PlayerDebugCommand extends BaseCommand {
 
-    public PlayerDebugCommand(mcMMO pluginRef) {
-        this.pluginRef = pluginRef;
-    }
+    @Dependency
+    private mcMMO plugin;
 
-    @Override
-    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+    @Default
+    public void onCommand(CommandSender sender) {
         if(sender instanceof Player) {
             BukkitMMOPlayer mcMMOPlayer = pluginRef.getUserManager().getPlayer((Player) sender);
             mcMMOPlayer.toggleDebugMode(); //Toggle debug mode
             pluginRef.getNotificationManager().sendPlayerInformationChatOnlyPrefixed(mcMMOPlayer.getNative(), "Commands.Mmodebug.Toggle", String.valueOf(mcMMOPlayer.isDebugMode()));
-            return true;
         } else {
-            return false;
+            //TODO: Localize
+            sender.sendMessage("Players only");
         }
     }
 

+ 0 - 1
mcmmo-core/src/main/java/com/gmail/nossr50/config/skills/ranks/SkillRankProperty.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.config.skills.ranks;
 import com.gmail.nossr50.api.exceptions.MissingSkillPropertyDefinition;
 import com.gmail.nossr50.datatypes.skills.properties.SkillProperty;
 import com.gmail.nossr50.mcMMO;
-import org.apache.logging.log4j.Level;
 
 import java.util.HashMap;
 

+ 1 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/config/sound/ConfigSound.java

@@ -30,6 +30,7 @@ public class ConfigSound {
         SOUND_SETTINGS_MAP_DEFAULT.put(SoundType.TIRED, new SoundSetting(1.0, 1.7));
         SOUND_SETTINGS_MAP_DEFAULT.put(SoundType.BLEED, new SoundSetting(2.0, 2.0));
         SOUND_SETTINGS_MAP_DEFAULT.put(SoundType.GLASS, new SoundSetting(1.0, 1.0));
+        SOUND_SETTINGS_MAP_DEFAULT.put(SoundType.ITEM_CONSUMED, new SoundSetting(1.0, 2.0));
     }
 
     @Setting(value = "Sound-Settings", comment = "Adjust sound settings for various mcMMO sounds here." +

+ 3 - 1
mcmmo-core/src/main/java/com/gmail/nossr50/core/MetadataConstants.java

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.core;
 
 import com.gmail.nossr50.mcmmo.api.platform.util.MetadataKey;
-
 import org.bukkit.metadata.FixedMetadataValue;
 
 /**
@@ -10,6 +9,7 @@ import org.bukkit.metadata.FixedMetadataValue;
 public class MetadataConstants {
 
     /* Metadata Values */
+    public static final MetadataKey<Boolean> REPLANT_META_KEY = new MetadataKey<>("mcMMO: Recently Replanted");
     public static final MetadataKey<Boolean> FISH_HOOK_REF_METAKEY = new MetadataKey<>("mcMMO: Fish Hook Tracker");
     public static final MetadataKey<Boolean> DODGE_TRACKER        = new MetadataKey<>("mcMMO: Dodge Tracker");
     public static final MetadataKey<Boolean> CUSTOM_DAMAGE_METAKEY = new MetadataKey<>("mcMMO: Custom Damage");
@@ -34,4 +34,6 @@ public class MetadataConstants {
     public final static MetadataKey<Boolean> PETS_ANIMAL_TRACKING_METAKEY = new MetadataKey<>("mcMMO: Pet Animal");
     public static final MetadataKey<Boolean> COTW_TEMPORARY_SUMMON = new MetadataKey<>("mcMMO: COTW Entity");
 
+    public static FixedMetadataValue metadataValue; //Gains value in onEnable
+
 }

+ 34 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/PlatformManager.java

@@ -0,0 +1,34 @@
+package com.gmail.nossr50.core;
+
+import com.gmail.nossr50.core.adapters.NMS_114.BukkitPlatformAdapter;
+import com.gmail.nossr50.core.adapters.PlatformAdapter;
+import com.gmail.nossr50.mcMMO;
+
+public class PlatformManager {
+    private PlatformAdapter platformAdapter;
+    private mcMMO pluginRef;
+
+    public PlatformManager(mcMMO pluginRef) {
+        this.pluginRef = pluginRef;
+        initAdapters();
+    }
+
+    /**
+     * Initialize the adapters based on the current platform
+     */
+    private void initAdapters() {
+        pluginRef.getLogger().info("Initializing platform adapters...");
+        //Determine which platform we are on and load the correct adapter
+        //For now this will be hardcoded for testing purposes
+        platformAdapter = new BukkitPlatformAdapter();
+    }
+
+    /**
+     * Get the current platform adapter implementation
+     * @return the current platform adapter
+     */
+    public PlatformAdapter getPlatformAdapter() {
+        return platformAdapter;
+    }
+
+}

+ 13 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/NBTAdapter.java

@@ -0,0 +1,13 @@
+package com.gmail.nossr50.core.adapters;
+
+import com.gmail.nossr50.core.nbt.NBTBase;
+
+public interface NBTAdapter {
+
+    /**
+     * Transform our NBT type representation to its implementation on the target platform
+     * @param nbtBase target NBT type representation
+     * @return platform specific implementation of our NBT Type
+     */
+    Object asNative(NBTBase nbtBase);
+}

+ 158 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/NMS_114/BukkitNBTAdapter.java

@@ -0,0 +1,158 @@
+package com.gmail.nossr50.core.adapters.NMS_114;
+
+import com.gmail.nossr50.core.adapters.NBTAdapter;
+import com.gmail.nossr50.core.nbt.NBTBase;
+import com.gmail.nossr50.core.nbt.NBTList;
+import com.gmail.nossr50.core.nbt.*;
+import net.minecraft.server.v1_14_R1.*;
+
+public class BukkitNBTAdapter implements NBTAdapter {
+
+    @Override
+    public Object asNative(NBTBase nbtBase) {
+        switch(nbtBase.getNBTType()) {
+            case END:
+                return new NBTTagEnd();
+            case BYTE:
+                return asNativeNBTByte((NBTByte) nbtBase);
+            case SHORT:
+                return asNativeNBTShort((NBTShort) nbtBase);
+            case INT:
+                return asNativeNBTInt((NBTInt) nbtBase);
+            case LONG:
+                return asNativeNBTLong((NBTLong) nbtBase);
+            case FLOAT:
+                return asNativeNBTFloat((NBTFloat) nbtBase);
+            case DOUBLE:
+                return asNativeNBTDouble((NBTDouble) nbtBase);
+            case BYTE_ARRAY:
+                return asNativeNBTByteArray((NBTByteArray) nbtBase);
+            case STRING:
+                return asNativeNBTString((NBTString) nbtBase);
+            case LIST:
+                return asNativeNBTList((NBTList) nbtBase);
+            case COMPOUND:
+                return asNativeNBTCompound((NBTCompound) nbtBase);
+            case INT_ARRAY:
+                return asNativeNBTIntArray((NBTIntArray) nbtBase);
+            case LONG_ARRAY:
+                return asNativeNBTLongArray((NBTLongArray) nbtBase);
+        }
+
+        return null;
+    }
+
+    /**
+     * Create a NBTTagByte (NMS Type) from our NBTByte representation
+     * @param nbtByte target NBTByte
+     * @return NBTTagByte copy of our NBTByte representation
+     */
+    private NBTTagByte asNativeNBTByte(NBTByte nbtByte) {
+        return new NBTTagByte(nbtByte.getValue());
+    }
+
+    /**
+     * Create a NBTTagShort (NMS Type) from our NBTShort representation
+     * @param nbtShort target NBTShort
+     * @return NBTTagShort copy of our NBTShort representation
+     */
+    private NBTTagShort asNativeNBTShort(NBTShort nbtShort) {
+        return new NBTTagShort(nbtShort.getValue());
+    }
+
+    /**
+     * Create a NBTTagInt (NMS Type) from our NBTInt representation
+     * @param nbtInt target NBTInt
+     * @return NBTTagInt copy of our NBTInt representation
+     */
+    private NBTTagInt asNativeNBTInt(NBTInt nbtInt) {
+        return new NBTTagInt(nbtInt.getValue());
+    }
+
+    /**
+     * Create a NBTTagLong (NMS Type) from our NBTLong representation
+     * @param nbtLong target NBTLong
+     * @return NBTTagLong copy of our NBTLong representation
+     */
+    private NBTTagLong asNativeNBTLong(NBTLong nbtLong) {
+        return new NBTTagLong(nbtLong.getValue());
+    }
+
+    /**
+     * Create a NBTTagFloat (NMS Type) from our NBTFloat representation
+     * @param nbtFloat target NBTFloat
+     * @return NBTTagFloat copy of our NBTFloat representation
+     */
+    private NBTTagFloat asNativeNBTFloat(NBTFloat nbtFloat) {
+        return new NBTTagFloat(nbtFloat.getValue());
+    }
+
+    /**
+     * Create a NBTTagDouble (NMS Type) from our NBTDouble representation
+     * @param nbtDouble target NBTDouble
+     * @return NBTTagDouble copy of our NBTDouble representation
+     */
+    private NBTTagDouble asNativeNBTDouble(NBTDouble nbtDouble) {
+        return new NBTTagDouble(nbtDouble.getValue());
+    }
+
+    /**
+     * Create a NBTTagByteArray (NMS Type) from our NBTByteArray representation
+     * @param nbtByteArray target NBTByteArray
+     * @return NBTTagByteArray copy of our NBTByteArray representation
+     */
+    private NBTTagByteArray asNativeNBTByteArray(NBTByteArray nbtByteArray) {
+        return new NBTTagByteArray(nbtByteArray.getValues());
+    }
+
+    /**
+     * Create a NBTTagString (NMS Type) from our NBTString representation
+     * @param nbtString target NBTString
+     * @return NBTTagString copy of our NBTString representation
+     */
+    private NBTTagString asNativeNBTString(NBTString nbtString) {
+        return new NBTTagString(nbtString.getValue());
+    }
+
+    /**
+     * Create a NBTTagList (NMS Type) from our NBTList representation
+     * @param nbtList target NBTList
+     * @return NBTTagList copy of our NBTList representation
+     */
+    private NBTTagList asNativeNBTList(NBTList nbtList) {
+        NBTTagList nbtTagList = new NBTTagList();
+        nbtList.setValues(nbtList.getValues());
+        return nbtTagList;
+    }
+
+    /**
+     * Create a NBTTagCompound (NMS Type) from our NBTCompound representation
+     * @param nbtCompound target NBTCompound
+     * @return NBTTagCompound copy of our NBTCompound representation
+     */
+    //TODO: Finish
+    private NBTTagCompound asNativeNBTCompound(NBTCompound nbtCompound) {
+        System.out.println("FINISH asNativeNBTCompound()");
+        NBTTagCompound nbtTagCompound = new NBTTagCompound();
+
+        return nbtTagCompound;
+    }
+
+    /**
+     * Create a NBTTagIntArray (NMS Type) from our NBTIntArray representation
+     * @param nbtIntArray target NBTIntArray
+     * @return NBTTagIntArray copy of our NBTIntArray representation
+     */
+    private NBTTagIntArray asNativeNBTIntArray(NBTIntArray nbtIntArray) {
+        return new NBTTagIntArray(nbtIntArray.getValues());
+    }
+
+    /**
+     * Create a NBTTagLongArray (NMS Type) from our NBTLongArray representation
+     * @param nbtLongArray target NBTLongArray
+     * @return NBTTagLongArray copy of our NBTLongArray representation
+     */
+    private NBTTagLongArray asNativeNBTLongArray(NBTLongArray nbtLongArray) {
+        return new NBTTagLongArray(nbtLongArray.getValues());
+    }
+}

+ 11 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/NMS_114/BukkitPlatformAdapter.java

@@ -0,0 +1,11 @@
+package com.gmail.nossr50.core.adapters.NMS_114;
+
+import com.gmail.nossr50.core.adapters.PlatformAdapter;
+
+public class BukkitPlatformAdapter extends PlatformAdapter {
+
+    public BukkitPlatformAdapter() {
+        super(new BukkitNBTAdapter());
+    }
+
+}

+ 19 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/adapters/PlatformAdapter.java

@@ -0,0 +1,19 @@
+package com.gmail.nossr50.core.adapters;
+
+public abstract class PlatformAdapter {
+
+    private NBTAdapter nbtAdapter; //nbt
+
+    public PlatformAdapter(NBTAdapter nbtAdapter) {
+        this.nbtAdapter = nbtAdapter;
+    }
+
+    /**
+     * Get the NBT Adapter for this platform
+     * @return the platform's NBT adapter
+     */
+    public NBTAdapter getNbtAdapter() {
+        return nbtAdapter;
+    }
+
+}

+ 11 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTBase.java

@@ -0,0 +1,11 @@
+package com.gmail.nossr50.core.nbt;
+
+public interface NBTBase {
+
+    /**
+     * Get the NBTType for this NBTBase
+     * @return this NBTType
+     */
+    NBTType getNBTType();
+
+}

+ 45 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTByte.java

@@ -0,0 +1,45 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Objects;
+
+public class NBTByte implements NBTBase {
+
+    private byte value;
+
+    public NBTByte(byte value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.BYTE;
+    }
+
+    public byte getValue() {
+        return value;
+    }
+
+    public void setValue(byte value) {
+        this.value = value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTByte nbtByte = (NBTByte) o;
+        return value == nbtByte.value;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    @Override
+    public String toString() {
+        return "NBTByte{" +
+                "value=" + value +
+                '}';
+    }
+}

+ 49 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTByteArray.java

@@ -0,0 +1,49 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Arrays;
+
+public class NBTByteArray implements NBTBase {
+
+    private byte[] values;
+
+    public NBTByteArray(byte[] values) {
+        this.values = values;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.BYTE_ARRAY;
+    }
+
+    public int getLength() {
+        return values.length;
+    }
+
+    public byte[] getValues() {
+        return values;
+    }
+
+    public void setValues(byte[] values) {
+        this.values = values;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTByteArray{" +
+                "values=" + Arrays.toString(values) +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTByteArray that = (NBTByteArray) o;
+        return Arrays.equals(values, that.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(values);
+    }
+}

+ 65 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTCompound.java

@@ -0,0 +1,65 @@
+package com.gmail.nossr50.core.nbt;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.util.*;
+
+public class NBTCompound implements NBTBase {
+
+    @NonNull
+    private Map<String, NBTBase> tagMap;
+
+    public NBTCompound() {
+        tagMap = new LinkedHashMap<>();
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.COMPOUND;
+    }
+
+    public NBTBase getTag(String key) {
+        return tagMap.get(key);
+    }
+
+    public void addNBT(String tagKey, NBTBase nbt) {
+        tagMap.put(tagKey, nbt);
+    }
+
+    public Collection<NBTBase> getMapValues() {
+        return tagMap.values();
+    }
+
+    public Set<String> getMapKeys() {
+        return tagMap.keySet();
+    }
+
+    public int getMapSize() {
+        return tagMap.size();
+    }
+
+    public void removeEntry(String tagKey) {
+        tagMap.remove(tagKey);
+    }
+
+    @Override
+    public String toString() {
+        return "NBTCompound{" +
+                "tagMap=" + tagMap +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTCompound that = (NBTCompound) o;
+        return tagMap.equals(that.tagMap);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(tagMap);
+    }
+}
+

+ 45 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTDouble.java

@@ -0,0 +1,45 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Objects;
+
+public class NBTDouble implements NBTBase {
+
+    private double value;
+
+    public NBTDouble(double value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.DOUBLE;
+    }
+
+    public double getValue() {
+        return value;
+    }
+
+    public void setValue(double value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTDouble{" +
+                "value=" + value +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTDouble nbtDouble = (NBTDouble) o;
+        return Double.compare(nbtDouble.value, value) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+}

+ 8 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTEnd.java

@@ -0,0 +1,8 @@
+package com.gmail.nossr50.core.nbt;
+
+public class NBTEnd implements NBTBase {
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.END;
+    }
+}

+ 45 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTFloat.java

@@ -0,0 +1,45 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Objects;
+
+public class NBTFloat implements NBTBase {
+
+    private float value;
+
+    public NBTFloat(float value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.FLOAT;
+    }
+
+    public float getValue() {
+        return value;
+    }
+
+    public void setValue(float value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTFloat{" +
+                "value=" + value +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTFloat nbtFloat = (NBTFloat) o;
+        return Float.compare(nbtFloat.value, value) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+}

+ 45 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTInt.java

@@ -0,0 +1,45 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Objects;
+
+public class NBTInt implements NBTBase {
+
+    private int value;
+
+    public NBTInt(int value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.INT;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    public void setValue(int value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTInt{" +
+                "value=" + value +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTInt nbtInt = (NBTInt) o;
+        return value == nbtInt.value;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+}

+ 49 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTIntArray.java

@@ -0,0 +1,49 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Arrays;
+
+public class NBTIntArray implements NBTBase {
+
+    private int[] values;
+
+    public NBTIntArray(int[] values) {
+        this.values = values;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.INT_ARRAY;
+    }
+
+    public int getLength() {
+        return values.length;
+    }
+
+    public int[] getValues() {
+        return values;
+    }
+
+    public void setValues(int[] values) {
+        this.values = values;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTIntArray{" +
+                "values=" + Arrays.toString(values) +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTIntArray that = (NBTIntArray) o;
+        return Arrays.equals(values, that.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(values);
+    }
+}

+ 53 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTList.java

@@ -0,0 +1,53 @@
+package com.gmail.nossr50.core.nbt;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.util.List;
+import java.util.Objects;
+
+public class NBTList implements NBTBase {
+
+    @NonNull
+    private List<? extends NBTBase> values;
+
+    public NBTList(@NonNull List<? extends NBTBase> values) {
+        this.values = values;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.LIST;
+    }
+
+    public int getLength() {
+        return values.size();
+    }
+
+    public List<? extends NBTBase> getValues() {
+        return values;
+    }
+
+    public void setValues(@NonNull List<? extends NBTBase> values) {
+        this.values = values;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTList nbtList = (NBTList) o;
+        return values.equals(nbtList.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(values);
+    }
+
+    @Override
+    public String toString() {
+        return "NBTList{" +
+                "values=" + values +
+                '}';
+    }
+}

+ 45 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTLong.java

@@ -0,0 +1,45 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Objects;
+
+public class NBTLong implements NBTBase {
+
+    private long value;
+
+    public NBTLong(long value) {
+        this.value = value;
+    }
+
+    public long getValue() {
+        return value;
+    }
+
+    public void setValue(long value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.LONG;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTLong{" +
+                "value=" + value +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTLong nbtLong = (NBTLong) o;
+        return value == nbtLong.value;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+}

+ 49 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTLongArray.java

@@ -0,0 +1,49 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Arrays;
+
+public class NBTLongArray implements NBTBase {
+
+    private long[] values;
+
+    public NBTLongArray(long[] values) {
+        this.values = values;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.LONG_ARRAY;
+    }
+
+    public int getLength() {
+        return values.length;
+    }
+
+    public long[] getValues() {
+        return values;
+    }
+
+    public void setValues(long[] values) {
+        this.values = values;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTLongArray{" +
+                "values=" + Arrays.toString(values) +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTLongArray that = (NBTLongArray) o;
+        return Arrays.equals(values, that.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(values);
+    }
+}

+ 45 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTShort.java

@@ -0,0 +1,45 @@
+package com.gmail.nossr50.core.nbt;
+
+import java.util.Objects;
+
+public class NBTShort implements NBTBase {
+
+    private short value;
+
+    public NBTShort(short value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.SHORT;
+    }
+
+    public short getValue() {
+        return value;
+    }
+
+    public void setValue(short value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTShort{" +
+                "value=" + value +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTShort nbtShort = (NBTShort) o;
+        return value == nbtShort.value;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+}

+ 50 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTString.java

@@ -0,0 +1,50 @@
+package com.gmail.nossr50.core.nbt;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public class NBTString implements NBTBase {
+
+    @NonNull
+    private String value;
+
+    public NBTString(@NonNull String value) {
+        this.value = value;
+    }
+
+    @Override
+    public NBTType getNBTType() {
+        return NBTType.STRING;
+    }
+
+    @NotNull
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(@NotNull String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "NBTString{" +
+                "value='" + value + '\'' +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NBTString nbtString = (NBTString) o;
+        return value.equals(nbtString.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+}

+ 22 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/core/nbt/NBTType.java

@@ -0,0 +1,22 @@
+package com.gmail.nossr50.core.nbt;
+
+/**
+ * Represents the NBT Type
+ * Based on NBT Structure in 1.14.4
+ */
+public enum NBTType {
+    ////String[] a = new String[]{"END", "BYTE", "SHORT", "INT", "LONG", "FLOAT", "DOUBLE", "BYTE[]", "STRING", "LIST", "COMPOUND", "INT[]", "LONG[]"};
+    END,
+    BYTE,
+    SHORT,
+    INT,
+    LONG,
+    FLOAT,
+    DOUBLE,
+    BYTE_ARRAY,
+    STRING,
+    LIST,
+    COMPOUND,
+    INT_ARRAY,
+    LONG_ARRAY
+}

+ 1 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/datatypes/meta/OldName.java

@@ -11,4 +11,5 @@ public class OldName extends FixedMetadataValue {
     public OldName(String oldName, mcMMO plugin) {
         super(plugin, oldName);
     }
+
 }

+ 17 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/datatypes/meta/RecentlyReplantedCropMeta.java

@@ -0,0 +1,17 @@
+package com.gmail.nossr50.datatypes.meta;
+
+import org.bukkit.metadata.FixedMetadataValue;
+import org.bukkit.plugin.Plugin;
+
+public class RecentlyReplantedCropMeta extends FixedMetadataValue {
+
+    /**
+     * Initializes a FixedMetadataValue with an Object
+     *
+     * @param owningPlugin the {@link Plugin} that created this metadata value
+     */
+    public RecentlyReplantedCropMeta(Plugin owningPlugin, Boolean recentlyPlanted) {
+        super(owningPlugin, recentlyPlanted);
+    }
+
+}

+ 11 - 19
mcmmo-core/src/main/java/com/gmail/nossr50/datatypes/skills/behaviours/HerbalismBehaviour.java

@@ -1,6 +1,5 @@
 package com.gmail.nossr50.datatypes.skills.behaviours;
 
-import com.gmail.nossr50.core.MetadataConstants;
 import com.gmail.nossr50.mcMMO;
 import org.bukkit.Material;
 import org.bukkit.block.BlockState;
@@ -23,7 +22,8 @@ public class HerbalismBehaviour {
     /**
      * Convert blocks affected by the Green Thumb & Green Terra abilities.
      *
-     * @param blockState The {@link BlockState} to check ability activation for
+     * @param blockState
+     *            The {@link BlockState} to check ability activation for
      * @return true if the ability was successful, false otherwise
      */
     public boolean convertGreenTerraBlocks(BlockState blockState) {
@@ -36,16 +36,16 @@ public class HerbalismBehaviour {
                 blockState.setType(Material.MOSSY_STONE_BRICKS);
                 return true;
 
-            case DIRT:
-            case GRASS_PATH:
+            case DIRT :
+            case GRASS_PATH :
                 blockState.setType(Material.GRASS_BLOCK);
                 return true;
 
-            case COBBLESTONE:
+            case COBBLESTONE :
                 blockState.setType(Material.MOSSY_COBBLESTONE);
                 return true;
 
-            default:
+            default :
                 return false;
         }
     }
@@ -53,29 +53,21 @@ public class HerbalismBehaviour {
     /**
      * Convert blocks affected by the Green Thumb & Green Terra abilities.
      *
-     * @param blockState The {@link BlockState} to check ability activation for
+     * @param blockState
+     *            The {@link BlockState} to check ability activation for
      * @return true if the ability was successful, false otherwise
      */
     public boolean convertShroomThumb(BlockState blockState) {
         switch (blockState.getType()) {
-            case DIRT:
+            case DIRT :
             case GRASS_BLOCK:
-            case GRASS_PATH:
+            case GRASS_PATH :
                 blockState.setType(Material.MYCELIUM);
                 return true;
 
-            default:
+            default :
                 return false;
         }
     }
 
-    /**
-     * Check if the block has a recently grown crop from Green Thumb
-     *
-     * @param blockState The {@link BlockState} to check green thumb regrown for
-     * @return true if the block is recently regrown, false otherwise
-     */
-    public boolean isRecentlyRegrown(BlockState blockState) {
-        return blockState.hasMetadata(MetadataConstants.GREEN_THUMB_METAKEY) && !pluginRef.getSkillTools().cooldownExpired(blockState.getMetadata(MetadataConstants.GREEN_THUMB_METAKEY).get(0).asInt(), 1);
-    }
 }

+ 1 - 1
mcmmo-core/src/main/java/com/gmail/nossr50/dumpster/HolidayManager.java

@@ -370,7 +370,7 @@
 //        int levelTotal = Misc.getRandom().nextInt(1 + pluginRef.getUserManager().getPlayer(player).getSkillLevel(PrimarySkillType.MINING)) + 1;
 //        pluginRef.getSoundManager().sendSound(player, player.getLocation(), SoundType.LEVEL_UP);
 //        mcMMO.getNotificationManager().sendPlayerInformation(player, NotificationType.HOLIDAY, "Holiday.AprilFools.Levelup", StringUtils.getCapitalized(fakeSkillType.toString()), String.valueOf(levelTotal));
-////        ParticleEffectUtils.fireworkParticleShower(player, ALL_COLORS.get(Misc.getRandom().nextInt(ALL_COLORS.size())));
+////        pluginRef.getParticleEffectUtils().fireworkParticleShower(player, ALL_COLORS.get(Misc.getRandom().nextInt(ALL_COLORS.size())));
 //    }
 //
 //    public void registerAprilCommand() {

+ 330 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/dumpster/SalvageManager.java

@@ -0,0 +1,330 @@
+//package com.gmail.nossr50.skills.salvage;
+//
+//import com.gmail.nossr50.config.AdvancedConfig;
+//import com.gmail.nossr50.config.Config;
+//import com.gmail.nossr50.config.experience.ExperienceConfig;
+//import com.gmail.nossr50.datatypes.interactions.NotificationType;
+//import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+//import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+//import com.gmail.nossr50.datatypes.skills.SubSkillType;
+//import com.gmail.nossr50.locale.LocaleLoader;
+//import com.gmail.nossr50.mcMMO;
+//import com.gmail.nossr50.skills.SkillManager;
+//import com.gmail.nossr50.skills.salvage.salvageables.Salvageable;
+//import com.gmail.nossr50.util.EventUtils;
+//import com.gmail.nossr50.util.Misc;
+//import com.gmail.nossr50.util.Permissions;
+//import com.gmail.nossr50.util.StringUtils;
+//import com.gmail.nossr50.util.player.NotificationManager;
+//import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
+//import com.gmail.nossr50.util.random.RandomChanceUtil;
+//import com.gmail.nossr50.util.skills.RankUtils;
+//import com.gmail.nossr50.util.skills.SkillUtils;
+//import com.gmail.nossr50.util.sounds.SoundManager;
+//import com.gmail.nossr50.util.sounds.SoundType;
+//import org.bukkit.Location;
+//import org.bukkit.Material;
+//import org.bukkit.enchantments.Enchantment;
+//import org.bukkit.entity.Player;
+//import org.bukkit.inventory.ItemStack;
+//import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+//
+//import java.util.Map;
+//import java.util.Map.Entry;
+//
+//public class SalvageManager extends SkillManager {
+//    private boolean placedAnvil;
+//    private int     lastClick;
+//
+//    public SalvageManager(McMMOPlayer mcMMOPlayer) {
+//        super(mcMMOPlayer, PrimarySkillType.SALVAGE);
+//    }
+//
+//    /**
+//     * Handles notifications for placing an anvil.
+//     */
+//    public void placedAnvilCheck() {
+//        Player player = getPlayer();
+//
+//        if (getPlacedAnvil()) {
+//            return;
+//        }
+//
+//        if (Config.getInstance().getSalvageAnvilMessagesEnabled()) {
+//            NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Salvage.Listener.Anvil");
+//        }
+//
+//        if (Config.getInstance().getSalvageAnvilPlaceSoundsEnabled()) {
+//            SoundManager.sendSound(player, player.getLocation(), SoundType.ANVIL);
+//        }
+//
+//        togglePlacedAnvil();
+//    }
+//
+//    public void handleSalvage(Location location, ItemStack item) {
+//        Player player = getPlayer();
+//
+//        Salvageable salvageable = mcMMO.getSalvageableManager().getSalvageable(item.getType());
+//
+//        if (item.getItemMeta() != null && item.getItemMeta().isUnbreakable()) {
+//            NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Anvil.Unbreakable");
+//            return;
+//        }
+//
+//        // Permissions checks on material and item types
+//        if (!Permissions.salvageItemType(player, salvageable.getSalvageItemType())) {
+//            NotificationManager.sendPlayerInformation(player, NotificationType.NO_PERMISSION, "mcMMO.NoPermission");
+//            return;
+//        }
+//
+//        if (!Permissions.salvageMaterialType(player, salvageable.getSalvageMaterialType())) {
+//            NotificationManager.sendPlayerInformation(player, NotificationType.NO_PERMISSION, "mcMMO.NoPermission");
+//            return;
+//        }
+//
+//        /*int skillLevel = getSkillLevel();*/
+//        int minimumSalvageableLevel = salvageable.getMinimumLevel();
+//
+//        // Level check
+//        if (getSkillLevel() < minimumSalvageableLevel) {
+//            NotificationManager.sendPlayerInformation(player, NotificationType.REQUIREMENTS_NOT_MET, "Salvage.Skills.Adept.Level", String.valueOf(RankUtils.getUnlockLevel(SubSkillType.SALVAGE_ARCANE_SALVAGE)), StringUtils.getPrettyItemString(item.getType()));
+//            return;
+//        }
+//
+//        int potentialSalvageYield = Salvage.calculateSalvageableAmount(item.getDurability(), salvageable.getMaximumDurability(), salvageable.getMaximumQuantity());
+//
+//        if (potentialSalvageYield <= 0) {
+//            NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Salvage.Skills.TooDamaged");
+//            return;
+//        }
+//
+//        potentialSalvageYield = Math.min(potentialSalvageYield, getSalvageLimit()); // Always get at least something back, if you're capable of salvaging it.
+//
+//        player.getInventory().setItemInMainHand(new ItemStack(Material.AIR));
+//        location.add(0.5, 1, 0.5);
+//
+//        Map<Enchantment, Integer> enchants = item.getEnchantments();
+//
+//        ItemStack enchantBook = null;
+//        if (!enchants.isEmpty()) {
+//            enchantBook = arcaneSalvageCheck(enchants);
+//        }
+//
+//        //Lottery on Salvageable Amount
+//
+//        int lotteryResults = 1;
+//        int chanceOfSuccess = 99;
+//
+//        for(int x = 0; x < potentialSalvageYield-1; x++) {
+//
+//            if(RandomChanceUtil.rollDice(chanceOfSuccess, 100)) {
+//                chanceOfSuccess-=3;
+//                chanceOfSuccess = Math.max(chanceOfSuccess, 90);
+//
+//                lotteryResults+=1;
+//            }
+//        }
+//
+//        if(lotteryResults == potentialSalvageYield && potentialSalvageYield != 1 && RankUtils.isPlayerMaxRankInSubSkill(player, SubSkillType.SALVAGE_ARCANE_SALVAGE)) {
+//            NotificationManager.sendPlayerInformationChatOnly(player, "Salvage.Skills.Lottery.Perfect", String.valueOf(lotteryResults), StringUtils.getPrettyItemString(item.getType()));
+//        } else if(salvageable.getMaximumQuantity() == 1 || getSalvageLimit() >= salvageable.getMaximumQuantity()) {
+//            NotificationManager.sendPlayerInformationChatOnly(player,  "Salvage.Skills.Lottery.Normal", String.valueOf(lotteryResults), StringUtils.getPrettyItemString(item.getType()));
+//        } else {
+//            NotificationManager.sendPlayerInformationChatOnly(player,  "Salvage.Skills.Lottery.Untrained", String.valueOf(lotteryResults), StringUtils.getPrettyItemString(item.getType()));
+//        }
+//
+//        ItemStack salvageResults = new ItemStack(salvageable.getSalvageMaterial(), lotteryResults);
+//
+//        //Call event
+//        if (EventUtils.callSalvageCheckEvent(player, item, salvageResults, enchantBook).isCancelled()) {
+//            return;
+//        }
+//
+//        Location anvilLoc = location.clone();
+//        Location playerLoc = player.getLocation().clone();
+//        double distance = anvilLoc.distance(playerLoc);
+//
+//        double speedLimit = .6;
+//        double minSpeed = .3;
+//
+//        //Clamp the speed and vary it by distance
+//        double vectorSpeed = Math.min(speedLimit, Math.max(minSpeed, distance * .2));
+//
+//        //Add a very small amount of height
+//        anvilLoc.add(0, .1, 0);
+//
+//        if (enchantBook != null) {
+//            Misc.spawnItemTowardsLocation(anvilLoc.clone(), playerLoc.clone(), enchantBook, vectorSpeed);
+//        }
+//
+//        Misc.spawnItemTowardsLocation(anvilLoc.clone(), playerLoc.clone(), salvageResults, vectorSpeed);
+//
+//        // BWONG BWONG BWONG - CLUNK!
+//        if (Config.getInstance().getSalvageAnvilUseSoundsEnabled()) {
+//            SoundManager.sendSound(player, player.getLocation(), SoundType.ITEM_BREAK);
+//        }
+//
+//        NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Salvage.Skills.Success");
+//    }
+//
+//    /*public double getMaxSalvagePercentage() {
+//        return Math.min((((Salvage.salvageMaxPercentage / Salvage.salvageMaxPercentageLevel) * getSkillLevel()) / 100.0D), Salvage.salvageMaxPercentage / 100.0D);
+//    }*/
+//
+//    public int getSalvageLimit() {
+//        return (RankUtils.getRank(getPlayer(), SubSkillType.SALVAGE_SCRAP_COLLECTOR));
+//    }
+//
+//    /**
+//     * Gets the Arcane Salvage rank
+//     *
+//     * @return the current Arcane Salvage rank
+//     */
+//    public int getArcaneSalvageRank() {
+//        return RankUtils.getRank(getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE);
+//    }
+//
+//    /*public double getExtractFullEnchantChance() {
+//        int skillLevel = getSkillLevel();
+//
+//        for (Tier tier : Tier.values()) {
+//            if (skillLevel >= tier.getLevel()) {
+//                return tier.getExtractFullEnchantChance();
+//            }
+//        }
+//
+//        return 0;
+//    }
+//
+//    public double getExtractPartialEnchantChance() {
+//        int skillLevel = getSkillLevel();
+//
+//        for (Tier tier : Tier.values()) {
+//            if (skillLevel >= tier.getLevel()) {
+//                return tier.getExtractPartialEnchantChance();
+//            }
+//        }
+//
+//        return 0;
+//    }*/
+//
+//    public double getExtractFullEnchantChance() {
+//        if(Permissions.hasSalvageEnchantBypassPerk(getPlayer()))
+//            return 100.0D;
+//
+//        return AdvancedConfig.getInstance().getArcaneSalvageExtractFullEnchantsChance(getArcaneSalvageRank());
+//    }
+//
+//    public double getExtractPartialEnchantChance() {
+//        return AdvancedConfig.getInstance().getArcaneSalvageExtractPartialEnchantsChance(getArcaneSalvageRank());
+//    }
+//
+//    private ItemStack arcaneSalvageCheck(Map<Enchantment, Integer> enchants) {
+//        Player player = getPlayer();
+//
+//        if (!RankUtils.hasUnlockedSubskill(player, SubSkillType.SALVAGE_ARCANE_SALVAGE) || !Permissions.arcaneSalvage(player)) {
+//            NotificationManager.sendPlayerInformationChatOnly(player, "Salvage.Skills.ArcaneFailed");
+//            return null;
+//        }
+//
+//        ItemStack book = new ItemStack(Material.ENCHANTED_BOOK);
+//        EnchantmentStorageMeta enchantMeta = (EnchantmentStorageMeta) book.getItemMeta();
+//
+//        boolean downgraded = false;
+//        int arcaneFailureCount = 0;
+//
+//        for (Entry<Enchantment, Integer> enchant : enchants.entrySet()) {
+//
+//            int enchantLevel = enchant.getValue();
+//
+//            if(!ExperienceConfig.getInstance().allowUnsafeEnchantments()) {
+//                if(enchantLevel > enchant.getKey().getMaxLevel()) {
+//                    enchantLevel = enchant.getKey().getMaxLevel();
+//                }
+//            }
+//
+//            if (!Salvage.arcaneSalvageEnchantLoss
+//                    || Permissions.hasSalvageEnchantBypassPerk(player)
+//                    || RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getExtractFullEnchantChance(), getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE))) {
+//                enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel, true);
+//            }
+//            else if (enchantLevel > 1
+//                    && Salvage.arcaneSalvageDowngrades
+//                    && RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getExtractPartialEnchantChance(), getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE))) {
+//                enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel - 1, true);
+//                downgraded = true;
+//            } else {
+//                arcaneFailureCount++;
+//            }
+//        }
+//
+//        if(failedAllEnchants(arcaneFailureCount, enchants.entrySet().size()))
+//        {
+//            NotificationManager.sendPlayerInformationChatOnly(player,  "Salvage.Skills.ArcaneFailed");
+//            return null;
+//        } else if(downgraded)
+//        {
+//            NotificationManager.sendPlayerInformationChatOnly(player,  "Salvage.Skills.ArcanePartial");
+//        }
+//
+//        book.setItemMeta(enchantMeta);
+//        return book;
+//    }
+//
+//    private boolean failedAllEnchants(int arcaneFailureCount, int size) {
+//        return arcaneFailureCount == size;
+//    }
+//
+//    /**
+//     * Check if the player has tried to use an Anvil before.
+//     * @param actualize
+//     *
+//     * @return true if the player has confirmed using an Anvil
+//     */
+//    public boolean checkConfirmation(boolean actualize) {
+//        Player player = getPlayer();
+//        long lastUse = getLastAnvilUse();
+//
+//        if (!SkillUtils.cooldownExpired(lastUse, 3) || !Config.getInstance().getSalvageConfirmRequired()) {
+//            return true;
+//        }
+//
+//        if (!actualize) {
+//            return false;
+//        }
+//
+//        actualizeLastAnvilUse();
+//
+//        NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Skills.ConfirmOrCancel", LocaleLoader.getString("Salvage.Pretty.Name"));
+//
+//        return false;
+//    }
+//
+//    /*
+//     * Salvage Anvil Placement
+//     */
+//
+//    public boolean getPlacedAnvil() {
+//        return placedAnvil;
+//    }
+//
+//    public void togglePlacedAnvil() {
+//        placedAnvil = !placedAnvil;
+//    }
+//
+//    /*
+//     * Salvage Anvil Usage
+//     */
+//
+//    public int getLastAnvilUse() {
+//        return lastClick;
+//    }
+//
+//    public void setLastAnvilUse(int value) {
+//        lastClick = value;
+//    }
+//
+//    public void actualizeLastAnvilUse() {
+//        lastClick = (int) (System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR);
+//    }
+//}

+ 12 - 15
mcmmo-core/src/main/java/com/gmail/nossr50/listeners/BlockListener.java

@@ -397,27 +397,24 @@ public class BlockListener implements Listener {
             return;
         }
 
+        McMMOPlayer mcMMOPlayer = pluginRef.getUserManager().getPlayer(player);
         BlockState blockState = event.getBlock().getState();
         ItemStack heldItem = player.getInventory().getItemInMainHand();
 
-        if (pluginRef.getDynamicSettingsManager().getSkillBehaviourManager().getHerbalismBehaviour().isRecentlyRegrown(blockState)) {
-            event.setCancelled(true);
-            return;
-        }
 
         if (pluginRef.getItemTools().isSword(heldItem)) {
-            HerbalismManager herbalismManager = pluginRef.getUserManager().getPlayer(player).getHerbalismManager();
-
-            if (herbalismManager.canUseHylianLuck()) {
-                if (herbalismManager.processHylianLuck(blockState)) {
-                    blockState.update(true);
-                    event.setCancelled(true);
-                } else if (blockState.getType() == Material.FLOWER_POT) {
-                    blockState.setType(Material.AIR);
-                    blockState.update(true);
-                    event.setCancelled(true);
+                HerbalismManager herbalismManager = mcMMOPlayer.getHerbalismManager();
+
+                if (herbalismManager.canUseHylianLuck()) {
+                    if (herbalismManager.processHylianLuck(blockState)) {
+                        blockState.update(true);
+                        event.setCancelled(true);
+                    } else if (blockState.getType() == Material.FLOWER_POT) {
+                        blockState.setType(Material.AIR);
+                        blockState.update(true);
+                        event.setCancelled(true);
+                    }
                 }
-            }
         }
         /*else if (!heldItem.containsEnchantment(Enchantment.SILK_TOUCH)) {
             SmeltingManager smeltingManager = pluginRef.getUserManager().getPlayer(player).getSmeltingManager();

+ 8 - 5
mcmmo-core/src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -352,12 +352,15 @@ public class EntityListener implements Listener {
                     }
 
                     //Deflect checks
-                    UnarmedManager unarmedManager = pluginRef.getUserManager().getPlayer(defendingPlayer).getUnarmedManager();
+                    final McMMOPlayer mcMMOPlayer = pluginRef.getUserManager().getPlayer(defendingPlayer);
+                    if (mcMMOPlayer != null) {
+                        UnarmedManager unarmedManager = mcMMOPlayer.getUnarmedManager();
 
-                    if (unarmedManager.canDeflect()) {
-                        if(unarmedManager.deflectCheck()) {
-                            event.setCancelled(true);
-                            return;
+                        if (unarmedManager.canDeflect()) {
+                            if (unarmedManager.deflectCheck()) {
+                                event.setCancelled(true);
+                                return;
+                            }
                         }
                     }
                 } else {

+ 14 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java

@@ -900,4 +900,18 @@ public class PlayerListener implements Listener {
             }
         }
     }
+
+//    @EventHandler(priority = EventPriority.LOWEST)
+//    public void onDebugPlayerInteract(PlayerInteractEvent event) {
+//        if(pluginRef.getUserManager().getPlayer(event.getPlayer()) != null) {
+//            McMMOPlayer mcMMOPlayer = pluginRef.getUserManager().getPlayer(event.getPlayer());
+//            if(mcMMOPlayer.isDebugMode()) {
+//                switch(event.getAction()) {
+//                    case LEFT_CLICK_AIR:
+//                    case LEFT_CLICK_BLOCK:
+//                        pluginRef.getNbtManager().debugNBTInMainHandItem(event.getPlayer());
+//                }
+//            }
+//        }
+//    }
 }

+ 1 - 1
mcmmo-core/src/main/java/com/gmail/nossr50/locale/LocaleManager.java

@@ -124,7 +124,7 @@ public final class LocaleManager {
         }
     }
 
-    private static String addColors(String input) {
+    public static String addColors(String input) {
         input = input.replaceAll("\\Q[[BLACK]]\\E", ChatColor.BLACK.toString());
         input = input.replaceAll("\\Q[[DARK_BLUE]]\\E", ChatColor.DARK_BLUE.toString());
         input = input.replaceAll("\\Q[[DARK_GREEN]]\\E", ChatColor.DARK_GREEN.toString());

+ 24 - 9
mcmmo-core/src/main/java/com/gmail/nossr50/mcMMO.java

@@ -10,10 +10,11 @@ import com.gmail.nossr50.config.playerleveling.ConfigLeveling;
 import com.gmail.nossr50.config.scoreboard.ConfigScoreboard;
 import com.gmail.nossr50.core.DynamicSettingsManager;
 import com.gmail.nossr50.core.MaterialMapStore;
+import com.gmail.nossr50.core.PlatformManager;
 import com.gmail.nossr50.database.DatabaseManager;
 import com.gmail.nossr50.database.DatabaseManagerFactory;
 import com.gmail.nossr50.datatypes.skills.subskills.acrobatics.Roll;
-import com.gmail.nossr50.listeners.*;
+import com.gmail.nossr50.listeners.InteractionManager;
 import com.gmail.nossr50.locale.LocaleManager;
 import com.gmail.nossr50.mcmmo.api.McMMOApi;
 import com.gmail.nossr50.mcmmo.api.platform.PlatformProvider;
@@ -34,20 +35,17 @@ import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManagerFactory;
 import com.gmail.nossr50.util.commands.CommandRegistrationManager;
 import com.gmail.nossr50.util.commands.CommandTools;
 import com.gmail.nossr50.util.experience.FormulaManager;
+import com.gmail.nossr50.util.nbt.NBTManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.PlayerLevelTools;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.random.RandomChanceTools;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
-import com.gmail.nossr50.util.skills.CombatTools;
-import com.gmail.nossr50.util.skills.PerkUtils;
-import com.gmail.nossr50.util.skills.RankTools;
-import com.gmail.nossr50.util.skills.SkillTools;
+import com.gmail.nossr50.util.skills.*;
 import com.gmail.nossr50.util.sounds.SoundManager;
 import com.gmail.nossr50.worldguard.WorldGuardManager;
 import com.gmail.nossr50.worldguard.WorldGuardUtils;
 import net.shatteredlands.shatt.backup.ZipLibrary;
-
 import org.bukkit.ChatColor;
 import org.bukkit.Material;
 import org.bukkit.NamespacedKey;
@@ -56,7 +54,6 @@ import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.Recipe;
 import org.bukkit.inventory.ShapelessRecipe;
 import org.bukkit.inventory.meta.ItemMeta;
-import org.bukkit.plugin.PluginManager;
 
 import java.io.File;
 import java.io.IOException;
@@ -73,7 +70,7 @@ public class mcMMO implements McMMOApi {
     private FormulaManager formulaManager;
     private NotificationManager notificationManager;
     private CommandRegistrationManager commandRegistrationManager;
-//    private NBTManager nbtManager;
+    private NBTManager nbtManager;
     private PartyManager partyManager;
     private LocaleManager localeManager;
     private ChatManager chatManager;
@@ -82,9 +79,11 @@ public class mcMMO implements McMMOApi {
     private ScoreboardManager scoreboardManager;
     private SoundManager soundManager;
     private HardcoreManager hardcoreManager;
+    private PlatformManager platformManager;
     private WorldGuardManager worldGuardManager;
 
     /* Not-Managers but my naming scheme sucks */
+    private ParticleEffectUtils particleEffectUtils;
     private DatabaseManagerFactory databaseManagerFactory;
     private ChunkManagerFactory chunkManagerFactory;
     private CommandTools commandTools;
@@ -186,9 +185,10 @@ public class mcMMO implements McMMOApi {
 
                 scheduleTasks();
                 commandRegistrationManager = new CommandRegistrationManager(this);
+                commandRegistrationManager.registerACFCommands();
                 commandRegistrationManager.registerCommands();
 
-//                nbtManager = new NBTManager();
+                nbtManager = new NBTManager();
 
                 //Init Chunk Manager Factory
                 chunkManagerFactory = new ChunkManagerFactory(this);
@@ -263,6 +263,9 @@ public class mcMMO implements McMMOApi {
 
         //Init PerkUtils
         perkUtils = new PerkUtils(this);
+
+        //Init particle effect utils
+        particleEffectUtils = new ParticleEffectUtils(this);
     }
 
     private String getVersion() {
@@ -763,4 +766,16 @@ public class mcMMO implements McMMOApi {
     public PerkUtils getPerkUtils() {
         return perkUtils;
     }
+
+    public NBTManager getNbtManager() {
+        return nbtManager;
+    }
+
+    public PlatformManager getPlatformManager() {
+        return platformManager;
+    }
+
+    public ParticleEffectUtils getParticleEffectUtils() {
+        return particleEffectUtils;
+    }
 }

+ 1 - 2
mcmmo-core/src/main/java/com/gmail/nossr50/runnables/skills/BleedTimerTask.java

@@ -3,7 +3,6 @@ package com.gmail.nossr50.runnables.skills;
 import com.gmail.nossr50.datatypes.interactions.NotificationType;
 import com.gmail.nossr50.datatypes.skills.BleedContainer;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.sounds.SoundType;
 import org.bukkit.Bukkit;
 import org.bukkit.entity.LivingEntity;
@@ -172,7 +171,7 @@ public class BleedTimerTask extends BukkitRunnable {
                 //Play Bleed Sound
                 pluginRef.getSoundManager().worldSendSound(target.getWorld(), target.getLocation(), SoundType.BLEED);
 
-                ParticleEffectUtils.playBleedEffect(target);
+                pluginRef.getParticleEffectUtils().playBleedEffect(target);
             }
 
             //Lower Bleed Ticks

+ 100 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/runnables/skills/DelayedCropReplant.java

@@ -0,0 +1,100 @@
+package com.gmail.nossr50.runnables.skills;
+
+import com.gmail.nossr50.core.MetadataConstants;
+import com.gmail.nossr50.datatypes.meta.RecentlyReplantedCropMeta;
+import com.gmail.nossr50.mcMMO;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.data.Ageable;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.scheduler.BukkitRunnable;
+
+public class DelayedCropReplant extends BukkitRunnable {
+
+    private final int desiredCropAge;
+    private final Location cropLocation;
+    private final Material cropMaterial;
+    private boolean wasImmaturePlant;
+    private final BlockBreakEvent blockBreakEvent;
+    private final mcMMO pluginRef;
+
+    /**
+     * Replants a crop after a delay setting the age to desiredCropAge
+     * @param cropState target {@link BlockState}
+     * @param desiredCropAge desired age of the crop
+     */
+    public DelayedCropReplant(mcMMO pluginRef, BlockBreakEvent blockBreakEvent, BlockState cropState, int desiredCropAge, boolean wasImmaturePlant) {
+        this.pluginRef = pluginRef;
+        //The plant was either immature or something cancelled the event, therefor we need to treat it differently
+        this.blockBreakEvent = blockBreakEvent;
+        this.wasImmaturePlant = wasImmaturePlant;
+        this.cropMaterial = cropState.getType();
+        this.desiredCropAge = desiredCropAge;
+        this.cropLocation = cropState.getLocation();
+    }
+
+    @Override
+    public void run() {
+        Block cropBlock = cropLocation.getBlock();
+        BlockState currentState = cropBlock.getState();
+
+        //Remove the metadata marking the block as recently replanted
+        new markPlantAsOld(blockBreakEvent.getBlock().getLocation()).runTaskLater(pluginRef, 10);
+
+        if(blockBreakEvent.isCancelled()) {
+            wasImmaturePlant = true;
+        }
+
+        //Two kinds of air in Minecraft
+        if(currentState.getType().equals(cropMaterial) || currentState.getType().equals(Material.AIR) || currentState.getType().equals(Material.CAVE_AIR)) {
+//            if(currentState.getBlock().getRelative(BlockFace.DOWN))
+            //The space is not currently occupied by a block so we can fill it
+            cropBlock.setType(cropMaterial);
+
+            //Get new state (necessary?)
+            BlockState newState = cropBlock.getState();
+//            newState.update();
+
+            Ageable ageable = (Ageable) newState.getBlockData();
+
+            //Crop age should always be 0 if the plant was immature
+            if(wasImmaturePlant) {
+                ageable.setAge(0);
+            } else {
+                //Otherwise make the plant the desired age
+                ageable.setAge(desiredCropAge);
+            }
+
+            //Age the crop
+            newState.setBlockData(ageable);
+            newState.update(true);
+
+            //Play an effect
+            pluginRef.getParticleEffectUtils().playGreenThumbEffect(cropLocation);
+
+        }
+
+    }
+
+    private class markPlantAsOld extends BukkitRunnable {
+
+        private final Location cropLoc;
+
+        public markPlantAsOld(Location cropLoc) {
+            this.cropLoc = cropLoc;
+        }
+
+        @Override
+        public void run() {
+            Block cropBlock = cropLoc.getBlock();
+
+            if(cropBlock.getMetadata(MetadataConstants.REPLANT_META_KEY).size() > 0) {
+                cropBlock.setMetadata(MetadataConstants.REPLANT_META_KEY, new RecentlyReplantedCropMeta(pluginRef, false));
+                pluginRef.getParticleEffectUtils().playFluxEffect(cropLocation);
+            }
+        }
+    }
+
+}

+ 1 - 2
mcmmo-core/src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java

@@ -10,7 +10,6 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.datatypes.skills.behaviours.AcrobaticsBehaviour;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.SkillManager;
-import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.Location;
 import org.bukkit.entity.Entity;
@@ -87,7 +86,7 @@ public class AcrobaticsManager extends SkillManager {
         Player player = getPlayer();
 
         if (!isFatal(modifiedDamage) && pluginRef.getRandomChanceTools().isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ACROBATICS_DODGE, player)) {
-            ParticleEffectUtils.playDodgeEffect(player);
+            pluginRef.getParticleEffectUtils().playDodgeEffect(player);
 
             if (mcMMOPlayer.useChatNotifications()) {
                 pluginRef.getNotificationManager().sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Combat.Proc");

+ 1 - 2
mcmmo-core/src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java

@@ -9,7 +9,6 @@ import com.gmail.nossr50.datatypes.skills.ToolType;
 import com.gmail.nossr50.datatypes.skills.behaviours.AxesBehaviour;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.SkillManager;
-import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.SkillActivationType;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.Player;
@@ -143,7 +142,7 @@ public class AxesManager extends SkillManager {
 
         Player player = getPlayer();
 
-        ParticleEffectUtils.playGreaterImpactEffect(target);
+        pluginRef.getParticleEffectUtils().playGreaterImpactEffect(target);
         target.setVelocity(player.getLocation().getDirection().normalize().multiply(pluginRef.getConfigManager().getConfigAxes().getGreaterImpactKnockBackModifier()));
 
         if (mcMMOPlayer.useChatNotifications()) {

+ 118 - 40
mcmmo-core/src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java

@@ -5,6 +5,7 @@ import com.gmail.nossr50.datatypes.BlockSnapshot;
 import com.gmail.nossr50.datatypes.experience.XPGainReason;
 import com.gmail.nossr50.datatypes.experience.XPGainSource;
 import com.gmail.nossr50.datatypes.interactions.NotificationType;
+import com.gmail.nossr50.datatypes.meta.RecentlyReplantedCropMeta;
 import com.gmail.nossr50.datatypes.player.BukkitMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
@@ -12,11 +13,12 @@ import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
 import com.gmail.nossr50.datatypes.skills.ToolType;
 import com.gmail.nossr50.datatypes.skills.behaviours.HerbalismBehaviour;
 import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.runnables.skills.DelayedCropReplant;
 import com.gmail.nossr50.runnables.skills.DelayedHerbalismXPCheckTask;
-import com.gmail.nossr50.runnables.skills.HerbalismBlockUpdaterTask;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.StringUtils;
 import com.gmail.nossr50.util.skills.SkillActivationType;
+import com.gmail.nossr50.util.sounds.SoundType;
 import org.bukkit.Material;
 import org.bukkit.block.Block;
 import org.bukkit.block.BlockFace;
@@ -27,7 +29,6 @@ import org.bukkit.entity.Player;
 import org.bukkit.event.block.BlockBreakEvent;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.PlayerInventory;
-import org.bukkit.metadata.FixedMetadataValue;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -134,6 +135,23 @@ public class HerbalismManager extends SkillManager {
             return;
         }
 
+        //Check if the plant was recently replanted
+        if(blockBreakEvent.getBlock().getBlockData() instanceof Ageable) {
+            Ageable ageableCrop = (Ageable) blockBreakEvent.getBlock().getBlockData();
+
+            if(blockBreakEvent.getBlock().getMetadata(MetadataConstants.REPLANT_META_KEY).size() >= 1) {
+                if(blockBreakEvent.getBlock().getMetadata(MetadataConstants.REPLANT_META_KEY).get(0).asBoolean()) {
+                    if(isAgeableMature(ageableCrop)) {
+                        blockBreakEvent.getBlock().removeMetadata(MetadataConstants.REPLANT_META_KEY, pluginRef);
+                    } else {
+                        //Crop is recently replanted to back out of destroying it
+                        blockBreakEvent.setCancelled(true);
+                        return;
+                    }
+                }
+            }
+        }
+
         /*
          * There are single-block plants and multi-block plants in Minecraft
          * In order to give out proper rewards, we need to collect all blocks that would be broken from this event
@@ -142,6 +160,9 @@ public class HerbalismManager extends SkillManager {
         //Grab all broken blocks
         HashSet<Block> brokenBlocks = getBrokenHerbalismBlocks(blockBreakEvent);
 
+        if(brokenBlocks.size() == 0)
+            return;
+
         //Handle rewards, xp, ability interactions, etc
         processHerbalismOnBlocksBroken(blockBreakEvent, brokenBlocks);
     }
@@ -153,10 +174,24 @@ public class HerbalismManager extends SkillManager {
      */
     private void processHerbalismOnBlocksBroken(BlockBreakEvent blockBreakEvent, HashSet<Block> brokenPlants) {
         BlockState originalBreak = blockBreakEvent.getBlock().getState();
+        boolean greenThumbActivated = false;
 
         //TODO: The design of Green Terra needs to change, this is a mess
         if(pluginRef.getPermissionTools().greenThumbPlant(getPlayer(), originalBreak.getType())) {
-            processGreenThumbPlants(originalBreak, isGreenTerraActive());
+            if(!getPlayer().isSneaking()) {
+                greenThumbActivated = processGreenThumbPlants(originalBreak, blockBreakEvent, isGreenTerraActive());
+            }
+
+        }
+
+        //When replanting a immature crop we cancel the block break event and back out
+        if(greenThumbActivated) {
+            if(originalBreak.getBlock().getBlockData() instanceof Ageable) {
+                Ageable ageableCrop = (Ageable) originalBreak.getBlock().getBlockData();
+                if(!isAgeableMature(ageableCrop)) {
+                    return;
+                }
+            }
         }
 
         /*
@@ -339,9 +374,11 @@ public class HerbalismManager extends SkillManager {
                 //Calculate XP
                 if(plantData instanceof Ageable) {
                     Ageable plantAgeable = (Ageable) plantData;
+
                     if(isAgeableMature(plantAgeable) || isBizarreAgeable(plantData)) {
                         xpToReward += pluginRef.getDynamicSettingsManager().getExperienceManager().getHerbalismXp(brokenBlockNewState.getType());
                     }
+
                 } else {
                     xpToReward += pluginRef.getDynamicSettingsManager().getExperienceManager().getHerbalismXp(brokenPlantBlock.getType());
                 }
@@ -437,8 +474,7 @@ public class HerbalismManager extends SkillManager {
     }
 
     private HashSet<Block> getBrokenChorusBlocks(BlockState originalBreak) {
-        HashSet<Block> traversedBlocks = grabChorusTreeBrokenBlocksRecursive(originalBreak.getBlock(), new HashSet<>());
-        return traversedBlocks;
+        return grabChorusTreeBrokenBlocksRecursive(originalBreak.getBlock(), new HashSet<>());
     }
 
     private HashSet<Block> grabChorusTreeBrokenBlocksRecursive(Block currentBlock, HashSet<Block> traversed) {
@@ -565,6 +601,7 @@ public class HerbalismManager extends SkillManager {
      * @param blockState The {@link BlockState} to check ability activation for
      * @return true if the ability was successful, false otherwise
      */
+    //TODO: Fix hylian luck? Do we give a #*$%?
     public boolean processHylianLuck(BlockState blockState) {
 //        if (!pluginRef.getRandomChanceTools().isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_HYLIAN_LUCK, getPlayer())) {
 //            return false;
@@ -631,15 +668,38 @@ public class HerbalismManager extends SkillManager {
         return herbalismBehaviour.convertShroomThumb(blockState);
     }
 
+    /**
+     * Starts the delayed replant task and turns
+     * @param desiredCropAge the desired age of the crop
+     * @param blockBreakEvent the {@link BlockBreakEvent} this crop was involved in
+     * @param cropState the {@link BlockState} of the crop
+     */
+    private void startReplantTask(int desiredCropAge, BlockBreakEvent blockBreakEvent, BlockState cropState, boolean isImmature) {
+        //Mark the plant as recently replanted to avoid accidental breakage
+        new DelayedCropReplant(pluginRef, blockBreakEvent, cropState, desiredCropAge, isImmature).runTaskLater(pluginRef, 20 * 2);
+        blockBreakEvent.getBlock().setMetadata(MetadataConstants.REPLANT_META_KEY, new RecentlyReplantedCropMeta(pluginRef, true));
+    }
+
     /**
      * Process the Green Thumb ability for plants.
      *
      * @param blockState The {@link BlockState} to check ability activation for
      * @param greenTerra boolean to determine if greenTerra is active or not
      */
-    private void processGreenThumbPlants(BlockState blockState, boolean greenTerra) {
-        if (!pluginRef.getBlockTools().isFullyGrown(blockState))
-            return;
+    private boolean processGreenThumbPlants(BlockState blockState, BlockBreakEvent blockBreakEvent, boolean greenTerra) {
+        if(!pluginRef.getItemTools().isHoe(blockBreakEvent.getPlayer().getInventory().getItemInMainHand())) {
+            return false;
+        }
+
+        BlockData blockData = blockState.getBlockData();
+
+        if (!(blockData instanceof Ageable)) {
+            return false;
+        }
+
+        Ageable ageable = (Ageable) blockData;
+
+        //If the ageable is NOT mature and the player is NOT using a hoe, abort
 
         Player player = getPlayer();
         PlayerInventory playerInventory = player.getInventory();
@@ -671,36 +731,49 @@ public class HerbalismManager extends SkillManager {
                 break;
 
             default:
-                return;
+                return false;
         }
 
         ItemStack seedStack = new ItemStack(seed);
 
         if (!greenTerra && !pluginRef.getRandomChanceTools().checkRandomChanceExecutionSuccess(player, SubSkillType.HERBALISM_GREEN_THUMB)) {
-            return;
+            return false;
         }
 
-        if (!processGrowingPlants(blockState, greenTerra)) {
-            return;
-        }
 
-        if (!pluginRef.getItemTools().isHoe(getPlayer().getInventory().getItemInMainHand())) {
-            if (!playerInventory.containsAtLeast(seedStack, 1)) {
-                return;
-            }
+        if (!playerInventory.containsAtLeast(seedStack, 1)) {
+            return false;
+        }
 
-            playerInventory.removeItem(seedStack);
-            player.updateInventory(); // Needed until replacement available
+        if (!processGrowingPlants(blockState, ageable, blockBreakEvent, greenTerra)) {
+            return false;
         }
 
-        new HerbalismBlockUpdaterTask(blockState).runTaskLater(pluginRef, 0);
+        playerInventory.removeItem(seedStack);
+        player.updateInventory(); // Needed until replacement available
+        //Play sound
+        pluginRef.getSoundManager().sendSound(player, player.getLocation(), SoundType.ITEM_CONSUMED);
+        return true;
+//        new HerbalismBlockUpdaterTask(blockState).runTaskLater(mcMMO.p, 0);
     }
 
-    private boolean processGrowingPlants(BlockState blockState, boolean greenTerra) {
-        int greenThumbStage = getGreenThumbStage();
+    private boolean processGrowingPlants(BlockState blockState, Ageable ageable, BlockBreakEvent blockBreakEvent, boolean greenTerra) {
+        //This check is needed
+        if(isBizarreAgeable(ageable)) {
+            return false;
+        }
 
-        blockState.setMetadata(MetadataConstants.GREEN_THUMB_METAKEY, new FixedMetadataValue(pluginRef, (int) (System.currentTimeMillis() / pluginRef.getMiscTools().TIME_CONVERSION_FACTOR)));
-        Ageable crops = (Ageable) blockState.getBlockData();
+        int finalAge = 0;
+        int greenThumbStage = getGreenThumbStage(greenTerra);
+
+        //Immature plants will start over at 0
+        if(!isAgeableMature(ageable)) {
+//            blockBreakEvent.setCancelled(true);
+            startReplantTask(0, blockBreakEvent, blockState, true);
+//            blockState.setType(Material.AIR);
+            blockBreakEvent.setDropItems(false);
+            return true;
+        }
 
         switch (blockState.getType()) {
 
@@ -708,42 +781,47 @@ public class HerbalismManager extends SkillManager {
             case CARROTS:
             case WHEAT:
 
-                if (greenTerra) {
-                    crops.setAge(3);
-                } else {
-                    crops.setAge(greenThumbStage);
-                }
+                    finalAge = getGreenThumbStage(greenTerra);
                 break;
 
             case BEETROOTS:
             case NETHER_WART:
 
                 if (greenTerra || greenThumbStage > 2) {
-                    crops.setAge(2);
-                } else if (greenThumbStage == 2) {
-                    crops.setAge(1);
-                } else {
-                    crops.setAge(0);
+                    finalAge = 2;
+                }
+                else if (greenThumbStage == 2) {
+                    finalAge = 1;
+                }
+                else {
+                    finalAge = 0;
                 }
                 break;
 
             case COCOA:
 
-                if (greenTerra || getGreenThumbStage() > 1) {
-                    crops.setAge(1);
-                } else {
-                    crops.setAge(0);
+                if (getGreenThumbStage(greenTerra) >= 2) {
+                    finalAge = 1;
+                }
+                else {
+                    finalAge = 0;
                 }
                 break;
 
             default:
                 return false;
         }
-        blockState.setBlockData(crops);
+
+        //Start the delayed replant
+        startReplantTask(finalAge, blockBreakEvent, blockState, false);
         return true;
     }
 
-    private int getGreenThumbStage() {
+    private int getGreenThumbStage(boolean greenTerraActive) {
+        if(greenTerraActive)
+            return Math.min(pluginRef.getRankTools().getHighestRank(SubSkillType.HERBALISM_GREEN_THUMB),
+                    pluginRef.getRankTools().getRank(getPlayer(), SubSkillType.HERBALISM_GREEN_THUMB) + 1);
+
         return pluginRef.getRankTools().getRank(getPlayer(), SubSkillType.HERBALISM_GREEN_THUMB);
     }
 }

+ 3 - 4
mcmmo-core/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java

@@ -13,7 +13,6 @@ import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.StringUtils;
 import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
-import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import com.gmail.nossr50.util.skills.SkillActivationType;
 import com.gmail.nossr50.util.sounds.SoundType;
 import org.bukkit.Location;
@@ -208,7 +207,7 @@ public class TamingManager extends SkillManager {
         if(!pluginRef.getRandomChanceTools().checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(pluginRef, pluginRef.getDynamicSettingsManager().getSkillPropertiesManager().getStaticChance(SubSkillType.TAMING_PUMMEL), getPlayer(), SubSkillType.TAMING_PUMMEL)))
             return;
 
-        ParticleEffectUtils.playGreaterImpactEffect(target);
+        pluginRef.getParticleEffectUtils().playGreaterImpactEffect(target);
         target.setVelocity(wolf.getLocation().getDirection().normalize().multiply(1.5D));
 
         if (target instanceof Player) {
@@ -381,7 +380,7 @@ public class TamingManager extends SkillManager {
         callOfWildEntity.setCustomName(pluginRef.getLocaleManager().getString("Taming.Summon.Name.Format", getPlayer().getName(), StringUtils.getPrettyEntityTypeString(entityType)));
 
         //Particle effect
-        ParticleEffectUtils.playCallOfTheWildEffect(callOfWildEntity);
+        pluginRef.getParticleEffectUtils().playCallOfTheWildEffect(callOfWildEntity);
     }
 
     private void spawnHorse(Location spawnLocation) {
@@ -408,7 +407,7 @@ public class TamingManager extends SkillManager {
         callOfWildEntity.setCustomName(pluginRef.getLocaleManager().getString("Taming.Summon.Name.Format", getPlayer().getName(), StringUtils.getPrettyEntityTypeString(EntityType.HORSE)));
 
         //Particle effect
-        ParticleEffectUtils.playCallOfTheWildEffect(callOfWildEntity);
+        pluginRef.getParticleEffectUtils().playCallOfTheWildEffect(callOfWildEntity);
     }
 
     private void setBaseCOTWEntityProperties(LivingEntity callOfWildEntity) {

+ 1 - 2
mcmmo-core/src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java

@@ -2,7 +2,6 @@ package com.gmail.nossr50.skills.taming;
 
 import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.skills.ParticleEffectUtils;
 import org.bukkit.Location;
 import org.bukkit.Sound;
 import org.bukkit.entity.LivingEntity;
@@ -39,7 +38,7 @@ public class TrackedTamingEntity extends BukkitRunnable {
         if (livingEntity.isValid()) {
             Location location = livingEntity.getLocation();
             location.getWorld().playSound(location, Sound.BLOCK_FIRE_EXTINGUISH, 0.8F, 0.8F);
-            ParticleEffectUtils.playCallOfTheWildEffect(livingEntity);
+            pluginRef.getParticleEffectUtils().playCallOfTheWildEffect(livingEntity);
             pluginRef.getCombatTools().dealDamage(livingEntity, livingEntity.getMaxHealth(), EntityDamageEvent.DamageCause.SUICIDE, livingEntity);
 
             if(tamingManagerRef != null)

+ 105 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/text/TextManager.java

@@ -0,0 +1,105 @@
+package com.gmail.nossr50.text;
+
+import com.gmail.nossr50.mcMMO;
+import net.kyori.text.TextComponent;
+import net.kyori.text.adapter.bukkit.TextAdapter;
+import net.kyori.text.serializer.gson.GsonComponentSerializer;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Handles some boiler plate related to kyori powered text library
+ */
+public class TextManager {
+    public static final char COLOR_CHAR = '§';
+    private mcMMO pluginRef;
+
+    public TextManager(mcMMO pluginRef) {
+        this.pluginRef = pluginRef;
+    }
+
+    /**
+     * Send a message to multiple recipients
+     * @param commandSenders target recipients
+     * @param textComponent the {@link TextComponent} to send
+     */
+    public void sendMessage(List<CommandSender> commandSenders, TextComponent textComponent) {
+        for(CommandSender commandSender : commandSenders) {
+            sendMessage(commandSender, textComponent);
+        }
+    }
+
+    /**
+     * Serializes and sends a text message to a specific recipient
+     * @param commandSender target recipient
+     * @param textComponent the {@link TextComponent} to serialize and send
+     */
+    public void sendMessage(CommandSender commandSender, TextComponent textComponent) {
+        String json = GsonComponentSerializer.INSTANCE.serialize(textComponent);
+        TextAdapter.sendMessage(commandSender, textComponent);
+    }
+
+    /**
+     * Sends a message to a single recipient with the (mcMMO) watermark at the beginning of the message
+     * @param commandSender target recipient
+     * @param textComponent the {@link TextComponent} to watermark and send
+     */
+    public void sendMessageWatermarked(CommandSender commandSender, TextComponent textComponent) {
+        TextComponent waterMarkedComponent = buildWaterMarked(textComponent);
+
+        sendMessage(commandSender, waterMarkedComponent);
+    }
+
+    /**
+     * Sends a message to a list of recipients with the (mcMMO) watermark at the beginning of the message
+     * @param commandSenders target recipients
+     * @param textComponent the {@link TextComponent} to watermark and send
+     */
+    public void sendMessageWatermarked(List<CommandSender> commandSenders, TextComponent textComponent) {
+        TextComponent waterMarkedComponent = buildWaterMarked(textComponent);
+
+        for(CommandSender commandSender : commandSenders) {
+            sendMessage(commandSender, waterMarkedComponent);
+        }
+    }
+
+    /**
+     * Builds a watermarked version of a text component
+     * @param textComponent target component to watermark
+     * @return a new {@link TextComponent} with the (mcMMO) watermark at the beginning and the contents of {@link TextComponent} appended afterwards
+     */
+    @NotNull
+    private TextComponent buildWaterMarked(TextComponent textComponent) {
+        return TextComponent.builder().content(pluginRef.getLocaleManager().getString("mcMMO.Template.Prefix")).append(textComponent).build();
+    }
+
+    /**
+     * Dissects a string and builds a {@link TextComponent} out of it.
+     * Results are cached to avoid needless operations in the future
+     * @param legacyText target text to transform
+     */
+    private TextComponent transformLegacyTexts(String legacyText) {
+        //TODO: Cache results
+        TextComponent.Builder builder = TextComponent.builder();
+
+        for(int i = 0; i < legacyText.toCharArray().length; i++) {
+            char c = legacyText.charAt(i);
+
+            //Found color character
+            if(c == COLOR_CHAR) {
+                if(i+1 >= legacyText.toCharArray().length) {
+                    //No color code because we're at the end of the string
+                    builder.append(String.valueOf(c));
+                } else {
+                    //TODO: finish
+                }
+            } else {
+                //Not a color character
+            }
+        }
+        return builder.build();
+    }
+
+}

+ 59 - 11
mcmmo-core/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java

@@ -1,6 +1,8 @@
 package com.gmail.nossr50.util.commands;
 
+import co.aikar.commands.PaperCommandManager;
 import com.gmail.nossr50.commands.*;
+import com.gmail.nossr50.commands.admin.NBTToolsCommand;
 import com.gmail.nossr50.commands.admin.PlayerDebugCommand;
 import com.gmail.nossr50.commands.admin.ReloadLocaleCommand;
 import com.gmail.nossr50.commands.chat.AdminChatCommand;
@@ -27,13 +29,70 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 
+//TODO: Properly rewrite ACF integration later
 public final class CommandRegistrationManager {
     private final mcMMO pluginRef;
     private String permissionsMessage;
+    //NOTE: Does not actually require paper, will work for bukkit
+    private PaperCommandManager commandManager;
 
     public CommandRegistrationManager(mcMMO pluginRef) {
         this.pluginRef = pluginRef;
         permissionsMessage = pluginRef.getLocaleManager().getString("mcMMO.NoPermission");
+        commandManager = new PaperCommandManager(pluginRef);
+    }
+
+    /**
+     * Register ACF Commands
+     */
+    //TODO: Properly rewrite ACF integration later
+    public void registerACFCommands() {
+        //Register ACF Commands
+        registerNBTToolsCommand();
+        registerMmoDebugCommand();
+    }
+
+    /**
+     * Register exception handlers for the ACF commands
+     */
+    //TODO: Properly rewrite ACF integration later
+    private void registerExceptionHandlers() {
+        registerDefaultExceptionHandler();
+    }
+
+    /**
+     * Register default exception handler
+     */
+    //TODO: Properly rewrite ACF integration later
+    private void registerDefaultExceptionHandler() {
+        commandManager.setDefaultExceptionHandler((command, registeredCommand, sender, args, t) -> {
+            pluginRef.getLogger().warning("Error occurred while executing command " + command.getName());
+            return false;
+        });
+    }
+
+    /**
+     * Register contexts for ACF
+     */
+    //TODO: Properly rewrite ACF integration later
+    private void registerContexts() {
+
+    }
+
+    /**
+     * Register the NBT Tools command
+     */
+    //TODO: Properly rewrite ACF integration later
+    private void registerNBTToolsCommand() {
+        commandManager.registerCommand(new NBTToolsCommand());
+    }
+
+    /**
+     * Register the MMO Debug command
+     */
+    //TODO: Properly rewrite ACF integration later
+    private void registerMmoDebugCommand() {
+        commandManager.registerCommand(new PlayerDebugCommand());
     }
 
     private void registerSkillCommands() {
@@ -153,16 +212,6 @@ public final class CommandRegistrationManager {
         command.setExecutor(new MmoInfoCommand(pluginRef));
     }
 
-
-    private void registerMmoDebugCommand() {
-        PluginCommand command = pluginRef.getCommand("mmodebug");
-        command.setDescription(pluginRef.getLocaleManager().getString("Commands.Description.mmodebug"));
-        command.setPermission(null); //No perm required to save support headaches
-        command.setPermissionMessage(permissionsMessage);
-        command.setUsage(pluginRef.getLocaleManager().getString("Commands.Usage.0", "mmodebug"));
-        command.setExecutor(new PlayerDebugCommand(pluginRef));
-    }
-
     private void registerMcChatSpyCommand() {
         PluginCommand command = pluginRef.getCommand("mcchatspy");
         command.setDescription(pluginRef.getLocaleManager().getString("Commands.Description.mcchatspy"));
@@ -427,7 +476,6 @@ public final class CommandRegistrationManager {
     public void registerCommands() {
         // Generic Commands
         registerMmoInfoCommand();
-        registerMmoDebugCommand();
         registerMcabilityCommand();
         registerMcgodCommand();
         registerMcChatSpyCommand();

+ 71 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/util/nbt/NBTFactory.java

@@ -0,0 +1,71 @@
+package com.gmail.nossr50.util.nbt;
+
+import com.gmail.nossr50.core.nbt.NBTByte;
+import net.minecraft.server.v1_14_R1.NBTTagByte;
+
+public class NBTFactory {
+    //TODO: Finish
+    /**
+     * Converts NMS NBT types into our own NBT type representation
+     * @param nmsNBT target NMS Compound
+     * @return NMS Representation of our NBT
+     */
+//    public NBTCompound asNBT(net.minecraft.server.v1_14_R1.NBTTagCompound nmsNBT) {
+//        NBTCompound nbtCompound = new NBTCompound("");
+//
+//        //Traverse the NMS Map
+//        for(String key : nmsNBT.getKeys()) {
+//
+//        }
+//    }
+
+    //TODO: Finish
+//    /**
+//     * Convert our NBT type into the NMS NBT Type equivalent
+//     * @param nbtCompound target nbt compound
+//     * @return NMS NBT copy of our NBT type
+//     */
+//    public net.minecraft.server.v1_14_R1.NBTTagCompound asNMSCopy(NBTCompound nbtCompound) {
+//
+//    }
+
+    /**
+     * Create a new NMS NBT tag compound with only 1 tag compound named "tag"
+     * @return new NMS NBT tag compound
+     */
+    private net.minecraft.server.v1_14_R1.NBTTagCompound makeNewNMSNBT() {
+        net.minecraft.server.v1_14_R1.NBTTagCompound nbtTagCompound = new net.minecraft.server.v1_14_R1.NBTTagCompound();
+
+        //Add the 'tag' compound where arbitrary data persists
+        nbtTagCompound.set("tag", new net.minecraft.server.v1_14_R1.NBTTagCompound());
+        return nbtTagCompound;
+    }
+
+    //TODO: Finish
+//    private NBTCompound deepCopy(NBTCompound target, String key, net.minecraft.server.v1_14_R1.NBTBase nbtBase) {
+//        switch (nbtBase.getTypeId()) {
+//            case 0:
+//                return new NBTCompound();
+//        }
+//    }
+
+    /**
+     * Create a NBTByte representation of NBTTagByte (NMS Type)
+     * @param nmsNBTByte target NMS NBTTagByte
+     * @return NBTByte representation of the targeted NMS nbt-type
+     */
+    private NBTByte asNBTByte(NBTTagByte nmsNBTByte) {
+        NBTByte nbtByte = new NBTByte(nmsNBTByte.asByte());
+        return nbtByte;
+    }
+
+    /**
+     * Create a NBTTagByte (NMS Type) from our NBTByte representation
+     * @param nbtByte target NBTByte
+     * @return NBTTagByte copy of our NBTByte representation
+     */
+    private NBTTagByte asNBTTagByte(NBTByte nbtByte) {
+        NBTTagByte nbtTagByte = new NBTTagByte(nbtByte.getValue());
+        return nbtTagByte;
+    }
+}

+ 167 - 17
mcmmo-core/src/main/java/com/gmail/nossr50/util/nbt/NBTManager.java

@@ -1,17 +1,22 @@
 package com.gmail.nossr50.util.nbt;
 
 
-import net.minecraft.server.v1_14_R1.NBTBase;
+import com.gmail.nossr50.core.nbt.NBTBase;
 import net.minecraft.server.v1_14_R1.NBTList;
 import net.minecraft.server.v1_14_R1.NBTTagCompound;
-import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
 import org.bukkit.craftbukkit.v1_14_R1.inventory.CraftItemStack;
 import org.bukkit.craftbukkit.v1_14_R1.util.CraftNBTTagConfigSerializer;
+import org.bukkit.entity.Player;
 import org.bukkit.inventory.ItemStack;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 
 public class NBTManager {
 
-    private static final String CRAFT_META_ITEM_CLASS_PATH = "org.bukkit.craftbukkit.inventory.CraftMetaItem";
+    private final String CRAFT_META_ITEM_CLASS_PATH = "org.bukkit.craftbukkit.inventory.CraftMetaItem";
     private Class<?> craftMetaItemClass;
 
     public NBTManager() {
@@ -19,21 +24,133 @@ public class NBTManager {
     }
 
     private void init() {
-        try {
-            Class<?> craftMetaItemClass = Class.forName(CRAFT_META_ITEM_CLASS_PATH); //for type comparisons
-        } catch (ClassNotFoundException e) {
-            e.printStackTrace();
-        }
+//        try {
+//            Class<?> craftMetaItemClass = Class.forName(CRAFT_META_ITEM_CLASS_PATH); //for type comparisons
+//        } catch (ClassNotFoundException e) {
+//            e.printStackTrace();
+//        }
     }
 
-    public static NBTTagCompound getNBT(ItemStack itemStack) {
-        Bukkit.broadcastMessage("Checking NBT for "+itemStack.toString());
+    /**
+     * Used for testing NBT stuff, will be deleted later
+     * @param player target player
+     */
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    //TODO: DELETE
+    public void debugNBTInMainHandItem(Player player) {
+        player.sendMessage("Starting NBT Debug Dump...");
+
+        ItemStack itemStack = player.getInventory().getItemInMainHand();
+        player.sendMessage("Checking NBT for "+itemStack.toString());
+
+        player.sendMessage("Total NBT Entries: "+getNBTCopy(player.getInventory().getItemInMainHand()).getKeys().size());
+        printNBT(player.getInventory().getItemInMainHand(), player);
+        player.sendMessage("-- END OF NBT REPORT --");
+
+        player.sendMessage("Attempting to add NBT key named - Herp");
+        addFloatNBT(player.getInventory().getItemInMainHand(), "herp", 13.37F);
+        player.updateInventory();
+
+        player.sendMessage("(After HERP) Total NBT Entries: "+getNBTCopy(player.getInventory().getItemInMainHand()).getKeys().size());
+        printNBT(player.getInventory().getItemInMainHand(), player);
+        player.sendMessage("-- END OF NBT REPORT --");
+
+        player.sendMessage("Attempting to save NBT data...");
+        player.updateInventory();
+    }
+
+    /**
+     * Gets the NMS.ItemStack Copy of a Bukkit.ItemStack
+     * @param itemStack target bukkit ItemStack
+     * @return the NMS.ItemStack "copy" of the Bukkit ItemStack
+     */
+    public net.minecraft.server.v1_14_R1.ItemStack getNMSItemStack(ItemStack itemStack) {
+        return CraftItemStack.asNMSCopy(itemStack);
+    }
+
+    /**
+     * Copies the NBT off an ItemStack and adds a tag compound if it doesn't exist
+     * @param itemStack target ItemStack
+     * @return the NBT copy of an ItemStack
+     */
+    @NonNull
+    public NBTTagCompound getNBTCopy(ItemStack itemStack) {
         net.minecraft.server.v1_14_R1.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
-        NBTTagCompound rootTag = nmsItemStack.getTag();
-        return rootTag;
+        NBTTagCompound freshNBTCopy = nmsItemStack.save(new NBTTagCompound());
+
+        if(!freshNBTCopy.hasKeyOfType("tag", 10)) {
+            freshNBTCopy.set("tag", new NBTTagCompound());
+        }
+
+        return freshNBTCopy;
+    }
+
+    /**
+     * Adds a Float Value to an ItemStack's NBT
+     * @param itemStack target ItemStack
+     * @param key the key for the new NBT float kv pair
+     * @param value the value of the new NBT float kv pair
+     */
+    public void addFloatNBT(ItemStack itemStack, String key, float value) {
+        //NBT Copied off Item
+        net.minecraft.server.v1_14_R1.ItemStack nmsIS = getNMSItemStack(itemStack);
+        NBTTagCompound freshNBTCopy = getNBTCopy(itemStack);
+
+        //New Float NBT Value
+        NBTTagCompound updatedNBT = new NBTTagCompound();
+        updatedNBT.setFloat(key, value);
+
+        //Merge
+        mergeToTagCompound(freshNBTCopy, updatedNBT);
+
+        //Invoke load() time
+        applyNBT(nmsIS, freshNBTCopy);
+
+        //Apply Item Meta (Not sure if needed)
+        CraftItemStack craftItemStack = CraftItemStack.asCraftMirror(nmsIS);
+        itemStack.setItemMeta(craftItemStack.getItemMeta());
+    }
+
+    /**
+     * Merges the modification compound into the target compound's tag NBT node
+     * @param targetCompound target NBT to merge into
+     * @param modificationCompound data to merge
+     */
+    public void mergeToTagCompound(NBTTagCompound targetCompound, NBTTagCompound modificationCompound) {
+        NBTTagCompound tagCompound = (NBTTagCompound) targetCompound.get("tag");
+        tagCompound.a(modificationCompound);
     }
 
-    public static NBTBase constructNBT(String nbtString) {
+    /**
+     * Applies NBT to an NMS.ItemStack
+     * @param nmsItemStack target NMS.ItemStack
+     * @param nbtTagCompound the new NBT data for the NMS.ItemStack
+     */
+    public void applyNBT(net.minecraft.server.v1_14_R1.ItemStack nmsItemStack, NBTTagCompound nbtTagCompound) {
+
+        try {
+            Class clazz = Class.forName("net.minecraft.server.v1_14_R1.ItemStack");
+            Class[] methodParameters = new Class[]{ NBTTagCompound.class };
+            Method loadMethod = clazz.getDeclaredMethod("load", methodParameters);
+            loadMethod.setAccessible(true);
+            loadMethod.invoke(nmsItemStack, nbtTagCompound);
+        } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public net.minecraft.server.v1_14_R1.NBTBase constructNBT(String nbtString) {
         try {
             return CraftNBTTagConfigSerializer.deserialize(nbtString);
         } catch (Exception e) {
@@ -43,13 +160,46 @@ public class NBTManager {
         }
     }
 
-    public static void printNBT(ItemStack itemStack) {
-        for(String key : getNBT(itemStack).getKeys()) {
-            Bukkit.broadcastMessage("NBT Key found: "+key);
+    /**
+     * Prints all the NBT KV pairs on an ItemStack
+     * @param itemStack target ItemStack
+     * @param player target player to send the message to
+     */
+    public void printNBT(ItemStack itemStack, Player player) {
+        NBTTagCompound tagCompoundCopy = getNBTCopy(itemStack);
+        printNBT(tagCompoundCopy, player);
+    }
+
+    private void printNBT(NBTTagCompound nbtTagCompound, Player player) {
+        for(String key : nbtTagCompound.getKeys()) {
+            player.sendMessage("");
+
+            net.minecraft.server.v1_14_R1.NBTBase targetTag = nbtTagCompound.get(key);
+
+            //Recursively print contents
+            if(targetTag instanceof NBTTagCompound) {
+                NBTTagCompound childTagCompound = nbtTagCompound.getCompound(key);
+                if(childTagCompound != null) {
+                    player.sendMessage(ChatColor.BLUE + "NBT named " + ChatColor.GOLD + key + ChatColor.BLUE + " is a tag compound, printing contents...");
+                    printNBT(childTagCompound, player);
+                    player.sendMessage(ChatColor.BLUE + "Exiting "+ key);
+                    continue;
+                }
+            }
+
+            player.sendMessage(ChatColor.GOLD + "Tag Key: " + ChatColor.RESET + key);
+
+            if(targetTag == null) {
+                player.sendMessage(ChatColor.RED + "Tag is null!");
+                continue;
+            }
+
+            player.sendMessage(ChatColor.GREEN + "Tag Value: " + ChatColor.RESET + targetTag.asString());
         }
+
     }
 
-    public static boolean hasNBT(NBTBase nbt, NBTTagCompound otherNbt) {
+    public boolean hasNBT(NBTBase nbt, NBTTagCompound otherNbt) {
         if(nbt instanceof NBTList<?>) {
 
         } else {

+ 9 - 2
mcmmo-core/src/main/java/com/gmail/nossr50/util/player/UserManager.java

@@ -8,6 +8,7 @@ import org.bukkit.OfflinePlayer;
 import org.bukkit.entity.Entity;
 import org.bukkit.entity.Player;
 import org.bukkit.metadata.FixedMetadataValue;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -51,9 +52,11 @@ public final class UserManager {
     public void remove(Player player) {
         BukkitMMOPlayer mcMMOPlayer = getPlayer(player);
         player.removeMetadata(MetadataConstants.PLAYER_DATA_METAKEY, pluginRef);
-        mcMMOPlayer.cleanup();
 
-        if(playerDataSet != null && playerDataSet.contains(mcMMOPlayer)) {
+        if(mcMMOPlayer != null)
+            mcMMOPlayer.cleanup();
+
+        if(playerDataSet != null) {
             playerDataSet.remove(mcMMOPlayer); //Clear sync save tracking
         }
     }
@@ -114,10 +117,12 @@ public final class UserManager {
      * @param playerName The name of the player whose McMMOPlayer to retrieve
      * @return the player's McMMOPlayer object
      */
+    @Nullable
     public BukkitMMOPlayer getPlayer(String playerName) {
         return retrieveMcMMOPlayer(playerName, false);
     }
 
+    @Nullable
     public BukkitMMOPlayer getOfflinePlayer(OfflinePlayer player) {
         if (player instanceof Player) {
             return getPlayer((Player) player);
@@ -136,6 +141,7 @@ public final class UserManager {
      * @param player target player
      * @return McMMOPlayer object for this player, null if Player has not been loaded
      */
+    @Nullable
     public BukkitMMOPlayer getPlayer(Player player) {
         //Avoid Array Index out of bounds
         if (player != null && player.hasMetadata(MetadataConstants.PLAYER_DATA_METAKEY))
@@ -144,6 +150,7 @@ public final class UserManager {
             return null;
     }
 
+    @Nullable
     private BukkitMMOPlayer retrieveMcMMOPlayer(String playerName, boolean offlineValid) {
         Player player = pluginRef.getServer().getPlayerExact(playerName);
 

+ 7 - 8
mcmmo-core/src/main/java/com/gmail/nossr50/util/skills/CombatTools.java

@@ -222,7 +222,7 @@ public final class CombatTools {
 
     }
 
-    private void processArcheryCombat(LivingEntity target, Player player, EntityDamageByEntityEvent event, Arrow arrow) {
+    private void processArcheryCombat(LivingEntity target, Player player, EntityDamageByEntityEvent event, Projectile arrow) {
         double initialDamage = event.getDamage();
 
         BukkitMMOPlayer mcMMOPlayer = pluginRef.getUserManager().getPlayer(player);
@@ -376,8 +376,9 @@ public final class CombatTools {
                     processTamingCombat(target, master, wolf, event);
                 }
             }
-        } else if (entityType == EntityType.ARROW) {
-            Arrow arrow = (Arrow) damager;
+        }
+        else if (entityType == EntityType.ARROW || entityType == EntityType.SPECTRAL_ARROW) {
+            Projectile arrow = (Projectile) damager;
             ProjectileSource projectileSource = arrow.getShooter();
 
             if (projectileSource instanceof Player && pluginRef.getSkillTools().canCombatSkillsTrigger(PrimarySkillType.ARCHERY, target)) {
@@ -407,11 +408,9 @@ public final class CombatTools {
         if (metadataValue.size() <= 0)
             return;
 
-        if (metadataValue != null) {
-            OldName oldName = (OldName) metadataValue.get(0);
-            entity.setCustomName(oldName.asString());
-            entity.setCustomNameVisible(false);
-        }
+        OldName oldName = (OldName) metadataValue.get(0);
+        entity.setCustomName(oldName.asString());
+        entity.setCustomNameVisible(false);
     }
 
 

+ 21 - 11
mcmmo-core/src/main/java/com/gmail/nossr50/util/skills/ParticleEffectUtils.java

@@ -1,5 +1,7 @@
 package com.gmail.nossr50.util.skills;
 
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.sounds.SoundType;
 import org.bukkit.Effect;
 import org.bukkit.Location;
 import org.bukkit.Material;
@@ -11,10 +13,19 @@ import org.bukkit.entity.Player;
 
 public final class ParticleEffectUtils {
 
-    private ParticleEffectUtils() {
+    private final mcMMO pluginRef;
+
+    public ParticleEffectUtils(mcMMO pluginRef) {
+        this.pluginRef = pluginRef;
+    }
+
+    public void playGreenThumbEffect(Location location) {
+        World world = location.getWorld();
+        playSmokeEffect(location);
+        pluginRef.getSoundManager().worldSendSoundMaxPitch(world, location, SoundType.POP);
     }
 
-    public static void playBleedEffect(LivingEntity livingEntity) {
+    public void playBleedEffect(LivingEntity livingEntity) {
         /*if (!MainConfig.getInstance().getBleedEffectEnabled()) {
             return;
         }*/
@@ -22,15 +33,15 @@ public final class ParticleEffectUtils {
         livingEntity.getWorld().playEffect(livingEntity.getEyeLocation(), Effect.STEP_SOUND, Material.REDSTONE_WIRE);
     }
 
-    public static void playDodgeEffect(Player player) {
+    public void playDodgeEffect(Player player) {
         /*if (!MainConfig.getInstance().getDodgeEffectEnabled()) {
             return;
         }*/
 
-        playSmokeEffect(player);
+        playSmokeEffect(player.getLocation());
     }
 
-    public static void playFluxEffect(Location location) {
+    public void playFluxEffect(Location location) {
         /*if (!MainConfig.getInstance().getFluxEffectEnabled()) {
             return;
         }*/
@@ -38,9 +49,8 @@ public final class ParticleEffectUtils {
         location.getWorld().playEffect(location, Effect.MOBSPAWNER_FLAMES, 1);
     }
 
-    public static void playSmokeEffect(LivingEntity livingEntity) {
-        Location location = livingEntity.getEyeLocation();
-        World world = livingEntity.getWorld();
+    public void playSmokeEffect(Location location) {
+        World world = location.getWorld();
 
         // Have to do it this way, because not all block directions are valid for smoke
         world.playEffect(location, Effect.SMOKE, BlockFace.SOUTH_EAST);
@@ -54,7 +64,7 @@ public final class ParticleEffectUtils {
         world.playEffect(location, Effect.SMOKE, BlockFace.NORTH_WEST);
     }
 
-    public static void playGreaterImpactEffect(LivingEntity livingEntity) {
+    public void playGreaterImpactEffect(LivingEntity livingEntity) {
         /*if (!MainConfig.getInstance().getGreaterImpactEffectEnabled()) {
             return;
         }*/
@@ -64,7 +74,7 @@ public final class ParticleEffectUtils {
         livingEntity.getWorld().createExplosion(location.getX(), location.getY(), location.getZ(), 0F, false, false);
     }
 
-    public static void playCallOfTheWildEffect(LivingEntity livingEntity) {
+    public void playCallOfTheWildEffect(LivingEntity livingEntity) {
         /*if (!MainConfig.getInstance().getCallOfTheWildEffectEnabled()) {
             return;
         }*/
@@ -87,7 +97,7 @@ public final class ParticleEffectUtils {
         firework.setFireworkMeta(fireworkMeta);
     }*/
 
-    private static boolean hasHeadRoom(Player player) {
+    private boolean hasHeadRoom(Player player) {
         boolean hasHeadRoom = true;
         Block headBlock = player.getEyeLocation().getBlock();
 

+ 7 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/util/sounds/SoundManager.java

@@ -43,6 +43,11 @@ public class SoundManager {
             world.playSound(location, getSound(soundType), getVolume(soundType), getPitch(soundType));
     }
 
+    public void worldSendSoundMaxPitch(World world, Location location, SoundType soundType) {
+        if(pluginRef.getConfigManager().getConfigSound().isSoundEnabled(soundType))
+            world.playSound(location, getSound(soundType), getVolume(soundType), 2.0F);
+    }
+
     /**
      * All volume is multiplied by the master volume to get its final value
      *
@@ -95,6 +100,8 @@ public class SoundManager {
                 return Sound.ENTITY_ENDER_EYE_DEATH;
             case GLASS:
                 return Sound.BLOCK_GLASS_BREAK;
+            case ITEM_CONSUMED:
+                return Sound.ITEM_BOTTLE_EMPTY;
             default:
                 return null;
         }

+ 1 - 0
mcmmo-core/src/main/java/com/gmail/nossr50/util/sounds/SoundType.java

@@ -15,6 +15,7 @@ public enum SoundType {
     ABILITY_ACTIVATED_BERSERK,
     BLEED,
     GLASS,
+    ITEM_CONSUMED,
     TIRED;
 
     public boolean usesCustomPitch() {

+ 1 - 1
mcmmo-core/src/main/resources/com/gmail/nossr50/locale/locale_en_US.properties

@@ -268,7 +268,7 @@ Herbalism.Ability.Lower=[[GRAY]]You lower your Hoe.
 Herbalism.Ability.Ready=[[DARK_AQUA]]You [[GOLD]]ready[[DARK_AQUA]] your Hoe.
 Herbalism.Ability.ShroomThumb.Fail=**SHROOM THUMB FAIL**
 Herbalism.SubSkill.GreenTerra.Name=Green Terra
-Herbalism.SubSkill.GreenTerra.Description=Spread the Terra, 3x Drops
+Herbalism.SubSkill.GreenTerra.Description=Spread the Terra, 3x Drops, Boosts Green Thumb
 Herbalism.SubSkill.GreenTerra.Stat=Green Terra Duration
 Herbalism.SubSkill.GreenThumb.Name=Green Thumb
 Herbalism.SubSkill.GreenThumb.Description=Auto-Plants crops when harvesting

+ 4 - 0
mcmmo-core/src/main/resources/sounds.yml

@@ -4,6 +4,10 @@ Sounds:
     # 1.0 = Max volume
     # 0.0 = No Volume
     MasterVolume: 1.0
+    ITEM_CONSUMED:
+        Enable: true
+        Volume: 1.0
+        Pitch: 1.0
     GLASS:
         Enable: true
         Volume: 1.0