فهرست منبع

Memory leak fixes, optimizations, and persistence

nossr50 4 سال پیش
والد
کامیت
20c69b63af

+ 22 - 1
Changelog.txt

@@ -1,9 +1,30 @@
 Version 2.1.148
-    Fixed a bug where weakness potions could prevent unarmed skills from activating and thus making unarmed useless
+    Fixed a memory leak involving entity metadata
     Alchemy progression is now more reasonable (delete skillranks.yml or edit it yourself to receive the change)
+    Made some optimizations to combat processing
+    New experience multiplier labeled 'Eggs' in experience.yml with a default value of 0 (previously mobs from eggs were using the Mobspawner experience multiplier)
+    New experience multiplier labeled 'Nether_Portal' in experience.yml with a default value of 0
+
+    Fixed a bug where mobs from eggs were only tracked if it was dispensed (egg tracking now tracks from egg items as well)
+    Fixed a bug where egg spawned mobs were sometimes not marked as being from an egg (used in experience multipliers)
+    Fixed a bug where entities transformed by a single event (such as lightning) weren't tracked properly if there was more than one entity involved
+    mmodebug now prints out some information about final damage when attacking an entity with certain weapons
+
+    (1.14+ required)
+        Mobs spawned from mob spawners are tracked persistently and are no longer forgotten about after a restart
+        Tamed mobs are tracked persistently and are no longer forgotten about after a restart
+        Egg spawned mobs are tracked persistently and are no longer forgotten about after a restart
+        Nether Portal spawned mobs are tracked persistently and are no longer forgotten about after a restart
+        Endermen who target endermite are tracked persistently and are no longer forgotten about after a restart
+        COTW spawned mobs are tracked persistently and are no longer forgotten about after a restart
+
 
     NOTES:
+    Egg mobs & Nether portal pigs being assigned to the mobspawner xp multiplier didn't make sense to me, so it has been changed. They have their own XP multipliers now.
+    While working on making data persistent I stumbled upon some alarming memory leak candidates, one of them was 7 years old. Sigh.
     Alchemy now progresses much smoother, with rank 2 no longer unlocking right away. Thanks to Momshroom for pointing out this oddity.
+    There's no persistent API for entities in Spigot for 1.13.2, but in the future I'll wire up NMS and write it to NBT myself.
+    This means the new persistence stuff requires 1.14.0 or higher, if you're still on 1.13.2 for now that stuff will behave like it always did
 
 Version 2.1.147
     Fixed a bug where players below the level threshold on a hardcore mode enabled server would gain levels on death in certain circumstances

+ 1 - 1
pom.xml

@@ -221,7 +221,7 @@
         <dependency>
             <groupId>org.spigotmc</groupId>
             <artifactId>spigot-api</artifactId>
-            <version>1.16.2-R0.1-SNAPSHOT</version>
+            <version>1.14-R0.1-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>

+ 7 - 0
src/main/java/com/gmail/nossr50/api/exceptions/IncompleteNamespacedKeyRegister.java

@@ -0,0 +1,7 @@
+package com.gmail.nossr50.api.exceptions;
+
+public class IncompleteNamespacedKeyRegister extends RuntimeException {
+    public IncompleteNamespacedKeyRegister(String message) {
+        super(message);
+    }
+}

+ 2 - 0
src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java

@@ -175,6 +175,8 @@ public class ExperienceConfig extends AutoUpdateConfigLoader {
 
     /* Spawned Mob modifier */
     public double getSpawnedMobXpMultiplier() { return config.getDouble("Experience_Formula.Mobspawners.Multiplier", 0.0); }
+    public double getEggXpMultiplier() { return config.getDouble("Experience_Formula.Eggs.Multiplier", 0.0); }
+    public double getNetherPortalXpMultiplier() { return config.getDouble("Experience_Formula.Nether_Portal.Multiplier", 0.0); }
     public double getBredMobXpMultiplier() { return config.getDouble("Experience_Formula.Breeding.Multiplier", 1.0); }
 
     /* Skill modifiers */

+ 102 - 145
src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -5,7 +5,6 @@ import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.config.WorldBlacklist;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
-import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
 import com.gmail.nossr50.datatypes.skills.subskills.interfaces.InteractType;
 import com.gmail.nossr50.events.fake.FakeEntityDamageByEntityEvent;
@@ -22,6 +21,8 @@ import com.gmail.nossr50.skills.unarmed.UnarmedManager;
 import com.gmail.nossr50.util.BlockUtils;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.compat.layers.persistentdata.AbstractPersistentDataLayer;
+import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.random.RandomChanceUtil;
@@ -46,30 +47,36 @@ import org.bukkit.metadata.FixedMetadataValue;
 import org.bukkit.potion.PotionEffect;
 import org.bukkit.potion.PotionEffectType;
 import org.bukkit.projectiles.ProjectileSource;
+import org.jetbrains.annotations.NotNull;
 
 public class EntityListener implements Listener {
-    private final mcMMO plugin;
+    private final mcMMO pluginRef;
+    private final @NotNull AbstractPersistentDataLayer persistentDataLayer;
 
-    public EntityListener(final mcMMO plugin) {
-        this.plugin = plugin;
+    public EntityListener(final mcMMO pluginRef) {
+        this.pluginRef = pluginRef;
+        persistentDataLayer = mcMMO.getCompatibilityManager().getPersistentDataLayer();
     }
 
     @EventHandler(priority = EventPriority.MONITOR)
-    public void onEntityTransform(EntityTransformEvent event)
-    {
-        //Transfer metadata keys from mob-spawned mobs to new mobs
-        if(event.getEntity().getMetadata(mcMMO.entityMetadataKey) != null || event.getEntity().getMetadata(mcMMO.entityMetadataKey).size() >= 1)
-        {
-            for(Entity entity : event.getTransformedEntities())
-            {
-                entity.setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
+    public void onEntityTransform(EntityTransformEvent event) {
+        if(event.getEntity() instanceof LivingEntity) {
+            LivingEntity livingEntity = (LivingEntity) event.getEntity();
+
+            //Transfer metadata keys from mob-spawned mobs to new mobs
+            if(persistentDataLayer.hasMobFlags(livingEntity)) {
+                for(Entity entity : event.getTransformedEntities()) {
+                    if(entity instanceof LivingEntity) {
+                        LivingEntity transformedEntity = (LivingEntity) entity;
+                        persistentDataLayer.addMobFlags(livingEntity, transformedEntity);
+                    }
+                }
             }
         }
     }
 
     @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
-    public void onEntityTargetEntity(EntityTargetLivingEntityEvent event)
-    {
+    public void onEntityTargetEntity(EntityTargetLivingEntityEvent event) {
         if(!ExperienceConfig.getInstance().isEndermanEndermiteFarmingPrevented())
             return;
 
@@ -82,8 +89,13 @@ public class EntityListener implements Listener {
         //Prevent entities from giving XP if they target endermite
         if(event.getTarget() instanceof Endermite)
         {
-            if(!event.getEntity().hasMetadata(mcMMO.entityMetadataKey))
-                event.getEntity().setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
+            if(event.getEntity() instanceof Enderman) {
+                Enderman enderman = (Enderman) event.getEntity();
+
+                if(!persistentDataLayer.hasMobFlag(MobMetaFlagType.EXPLOITED_ENDERMEN, enderman)) {
+                    persistentDataLayer.flagMetadata(MobMetaFlagType.EXPLOITED_ENDERMEN, enderman);
+                }
+            }
         }
     }
 
@@ -119,8 +131,8 @@ public class EntityListener implements Listener {
             projectile.setMetadata(mcMMO.infiniteArrowKey, mcMMO.metadataValue);
         }
 
-        projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(plugin, Math.min(event.getForce() * AdvancedConfig.getInstance().getForceMultiplier(), 1.0)));
-        projectile.setMetadata(mcMMO.arrowDistanceKey, new FixedMetadataValue(plugin, projectile.getLocation()));
+        projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, Math.min(event.getForce() * AdvancedConfig.getInstance().getForceMultiplier(), 1.0)));
+        projectile.setMetadata(mcMMO.arrowDistanceKey, new FixedMetadataValue(pluginRef, projectile.getLocation()));
     }
 
     @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
@@ -145,8 +157,8 @@ public class EntityListener implements Listener {
             if(!(projectile instanceof Arrow))
                 return;
 
-            projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(plugin, 1.0));
-            projectile.setMetadata(mcMMO.arrowDistanceKey, new FixedMetadataValue(plugin, projectile.getLocation()));
+            projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, 1.0));
+            projectile.setMetadata(mcMMO.arrowDistanceKey, new FixedMetadataValue(pluginRef, projectile.getLocation()));
 
             for(Enchantment enchantment : player.getInventory().getItemInMainHand().getEnchantments().keySet()) {
                 if(enchantment.getName().equalsIgnoreCase("piercing"))
@@ -185,74 +197,38 @@ public class EntityListener implements Listener {
         Entity entity = event.getEntity();
 
         if (entity instanceof FallingBlock || entity instanceof Enderman) {
-            boolean isTracked = entity.hasMetadata(mcMMO.entityMetadataKey);
-
-            if (mcMMO.getPlaceStore().isTrue(block) && !isTracked) {
-                mcMMO.getPlaceStore().setFalse(block);
-
-                entity.setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
-            }
-            else if (isTracked) {
-                mcMMO.getPlaceStore().setTrue(block);
-            }
-        } else if ((block.getType() == Material.REDSTONE_ORE)) {
-        }
-        else {
+            trackMovingBlocks(block, entity); //ignore the IDE warning
+        //Apparently redstone ore will throw these events
+        } else if ((block.getType() != Material.REDSTONE_ORE)) {
             if (mcMMO.getPlaceStore().isTrue(block)) {
                 mcMMO.getPlaceStore().setFalse(block);
             }
         }
     }
 
-    /*@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
-    public void onEntityDamageDebugLowest(EntityDamageEvent event)
-    {
-        if(event instanceof FakeEntityDamageByEntityEvent)
-            return;
-
-        if(event instanceof FakeEntityDamageEvent)
-            return;
+    /**
+     * This is a complex hack to track blocks for this event
+     * This event is called when a block starts its movement, or ends its movement
+     * It can start the movement through physics (falling blocks) or through being picked up (endermen)
+     * Since this event can be cancelled, its even weirder to track this stuff
+     * @param block this will either be the block that was originally picked up, or the block in its final destination
+     * @param movementSourceEntity this will either be an Endermen or a Falling Block
+     */
+    private void trackMovingBlocks(@NotNull Block block, @NotNull Entity movementSourceEntity) {
 
-        Bukkit.broadcastMessage(ChatColor.DARK_AQUA+"DMG Before Events: "
-                +ChatColor.RESET+event.getDamage());
+        //A block that has reached its destination, either being placed by endermen or having finished its fall
+        if(movementSourceEntity.hasMetadata(mcMMO.travelingBlock)) {
+            mcMMO.getPlaceStore().setTrue(block);
+            movementSourceEntity.removeMetadata(mcMMO.travelingBlock, pluginRef);
+        } else {
+            //A block that is starting movement (from either Endermen or Falling/Physics)
+            if(mcMMO.getPlaceStore().isTrue(block)) {
+                mcMMO.getPlaceStore().setFalse(block);
+                movementSourceEntity.setMetadata(mcMMO.blockMetadataKey, mcMMO.metadataValue);
+            }
+        }
     }
 
-    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
-    public void onEntityDamageDebugMonitor(EntityDamageEvent event)
-    {
-        if(event instanceof FakeEntityDamageByEntityEvent)
-            return;
-
-        if(event instanceof FakeEntityDamageEvent)
-            return;
-
-        if(!(event.getEntity() instanceof LivingEntity))
-            return;
-
-        LivingEntity entity = (LivingEntity) event.getEntity();
-
-        double rawDamage = event.getDamage();
-        double dmgAfterReduction = event.getFinalDamage();
-
-        Bukkit.broadcastMessage(ChatColor.GOLD+"DMG After Events: "
-                + event.getEntity().getName()+ChatColor.RESET
-                +"RawDMG["+rawDamage+"], "
-                +"FinalDMG=["+dmgAfterReduction+"]");
-
-        Bukkit.broadcastMessage(
-                event.getEntity().getName()
-                +ChatColor.GREEN
-                +" HP "
-                +ChatColor.RESET
-                +entity.getHealth()
-                +ChatColor.YELLOW
-                +" -> "
-                +ChatColor.RESET
-                +(entity.getHealth()-event.getFinalDamage()));
-
-        Bukkit.broadcastMessage("");
-    }*/
-
     @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
     public void onEntityCombustByEntityEvent(EntityCombustByEntityEvent event) {
         //Prevent players from setting fire to each other if they are in the same party
@@ -284,6 +260,10 @@ public class EntityListener implements Listener {
      */
     @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
     public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
+        if (event instanceof FakeEntityDamageByEntityEvent) {
+            return;
+        }
+
         double damage = event.getFinalDamage();
         Entity defender = event.getEntity();
         Entity attacker = event.getDamager();
@@ -292,35 +272,31 @@ public class EntityListener implements Listener {
         {
             if(attacker instanceof Player) {
 
-                if(!WorldGuardManager.getInstance().hasMainFlag((Player) attacker))
+                if(!WorldGuardManager.getInstance().hasMainFlag((Player) attacker)) {
                     return;
+                }
 
             } else if(attacker instanceof Projectile) {
 
                 Projectile projectile = (Projectile) attacker;
 
                 if(projectile.getShooter() instanceof Player) {
-                    if(!WorldGuardManager.getInstance().hasMainFlag((Player) projectile.getShooter()))
+                    if(!WorldGuardManager.getInstance().hasMainFlag((Player) projectile.getShooter())) {
                         return;
+                    }
                 }
 
             }
         }
 
         /* WORLD BLACKLIST CHECK */
-        if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
-            return;
-
-        if (event instanceof FakeEntityDamageByEntityEvent) {
+        if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) {
             return;
         }
 
         // Don't process this event for marked entities, for players this is handled above,
         // However, for entities, we do not wanna cancel this event to allow plugins to observe changes
         // properly
-        if (defender.getMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY).size() > 0) {
-            return;
-        }
 
         if (CombatUtils.isProcessingNoInvulnDamage()) {
             return;
@@ -344,6 +320,10 @@ public class EntityListener implements Listener {
             return;
         }
 
+        if (CombatUtils.hasIgnoreDamageMetadata(target)) {
+            return;
+        }
+
         if (attacker instanceof Tameable) {
             AnimalTamer animalTamer = ((Tameable) attacker).getOwner();
 
@@ -422,7 +402,7 @@ public class EntityListener implements Listener {
         }
 
         CombatUtils.processCombatAttack(event, attacker, target);
-        CombatUtils.handleHealthbars(attacker, target, event.getFinalDamage(), plugin);
+        CombatUtils.handleHealthbars(attacker, target, event.getFinalDamage(), pluginRef);
     }
 
     @EventHandler(priority =  EventPriority.MONITOR, ignoreCancelled = false)
@@ -490,7 +470,7 @@ public class EntityListener implements Listener {
          * Process Registered Interactions
          */
 
-        InteractionManager.processEvent(event, plugin, InteractType.ON_ENTITY_DAMAGE);
+        InteractionManager.processEvent(event, pluginRef, InteractType.ON_ENTITY_DAMAGE);
 
         /*
          * Old code
@@ -646,29 +626,7 @@ public class EntityListener implements Listener {
      */
     @EventHandler(priority = EventPriority.LOWEST)
     public void onEntityDeathLowest(EntityDeathEvent event) {
-        /* WORLD BLACKLIST CHECK */
-        if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
-            return;
-
-        LivingEntity entity = event.getEntity();
-
-        if (Misc.isNPCEntityExcludingVillagers(entity)) {
-            return;
-        }
-
-        if (entity.hasMetadata(mcMMO.customNameKey)) {
-            entity.setCustomName(entity.getMetadata(mcMMO.customNameKey).get(0).asString());
-            entity.removeMetadata(mcMMO.customNameKey, plugin);
-        }
-
-        if (entity.hasMetadata(mcMMO.customVisibleKey)) {
-            entity.setCustomNameVisible(entity.getMetadata(mcMMO.customVisibleKey).get(0).asBoolean());
-            entity.removeMetadata(mcMMO.customVisibleKey, plugin);
-        }
-
-        if (entity.hasMetadata(mcMMO.entityMetadataKey)) {
-            entity.removeMetadata(mcMMO.entityMetadataKey, plugin);
-        }
+        mcMMO.getTransientMetadataTools().cleanAllMobMetadata(event.getEntity());
     }
 
     /**
@@ -704,33 +662,41 @@ public class EntityListener implements Listener {
         if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
             return;
 
-        LivingEntity entity = event.getEntity();
+        LivingEntity livingEntity = event.getEntity();
 
         switch (event.getSpawnReason()) {
             case NETHER_PORTAL:
+                trackSpawnedAndPassengers(livingEntity, MobMetaFlagType.NETHER_PORTAL_MOB);
+                break;
             case SPAWNER:
             case SPAWNER_EGG:
-                entity.setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
-
-                Entity passenger = entity.getPassenger();
-
-                if (passenger != null) {
-                    passenger.setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
-                }
-                return;
-
+                trackSpawnedAndPassengers(livingEntity, MobMetaFlagType.MOB_SPAWNER_MOB);
+                break;
+            case DISPENSE_EGG:
+            case EGG:
+                trackSpawnedAndPassengers(livingEntity, MobMetaFlagType.EGG_MOB);
+                break;
             case BREEDING:
-                entity.setMetadata(mcMMO.bredMetadataKey, mcMMO.metadataValue);
-                return;
-
+                trackSpawnedAndPassengers(livingEntity, MobMetaFlagType.PLAYER_BRED_MOB);
+                break;
             default:
         }
     }
 
+    private void trackSpawnedAndPassengers(LivingEntity livingEntity, MobMetaFlagType mobMetaFlagType) {
+        persistentDataLayer.flagMetadata(mobMetaFlagType, livingEntity);
+
+        for(Entity passenger : livingEntity.getPassengers()) {
+            if(passenger != null) {
+                persistentDataLayer.flagMetadata(mobMetaFlagType, livingEntity);
+            }
+        }
+    }
+
     @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
     public void onEntityBreed(EntityBreedEvent event) {
         if(ExperienceConfig.getInstance().isCOTWBreedingPrevented()) {
-            if(event.getFather().hasMetadata(mcMMO.COTW_TEMPORARY_SUMMON) || event.getMother().hasMetadata(mcMMO.COTW_TEMPORARY_SUMMON)) {
+            if(persistentDataLayer.hasMobFlag(MobMetaFlagType.COTW_SUMMONED_MOB, event.getFather()) || persistentDataLayer.hasMobFlag(MobMetaFlagType.COTW_SUMMONED_MOB, event.getMother())) {
                 event.setCancelled(true);
                 Animals mom = (Animals) event.getMother();
                 Animals father = (Animals) event.getFather();
@@ -745,7 +711,6 @@ public class EntityListener implements Listener {
                     NotificationManager.sendPlayerInformationChatOnly(player, "Taming.Summon.COTW.BreedingDisallowed");
                 }
             }
-
         }
     }
 
@@ -769,7 +734,7 @@ public class EntityListener implements Listener {
 
         // We can make this assumption because we (should) be the only ones
         // using this exact metadata
-        Player player = plugin.getServer().getPlayerExact(entity.getMetadata(mcMMO.tntMetadataKey).get(0).asString());
+        Player player = pluginRef.getServer().getPlayerExact(entity.getMetadata(mcMMO.tntMetadataKey).get(0).asString());
 
         if (!UserManager.hasPlayerDataKey(player)) {
             return;
@@ -815,7 +780,7 @@ public class EntityListener implements Listener {
 
         // We can make this assumption because we (should) be the only ones
         // using this exact metadata
-        Player player = plugin.getServer().getPlayerExact(entity.getMetadata(mcMMO.tntMetadataKey).get(0).asString());
+        Player player = pluginRef.getServer().getPlayerExact(entity.getMetadata(mcMMO.tntMetadataKey).get(0).asString());
 
         if (!UserManager.hasPlayerDataKey(player)) {
             return;
@@ -983,13 +948,16 @@ public class EntityListener implements Listener {
                 return;
         }
 
-        LivingEntity entity = event.getEntity();
+        LivingEntity livingEntity = event.getEntity();
 
-        if (!UserManager.hasPlayerDataKey(player) || Misc.isNPCEntityExcludingVillagers(entity) || entity.hasMetadata(mcMMO.entityMetadataKey)) {
+        if (!UserManager.hasPlayerDataKey(player)
+                || Misc.isNPCEntityExcludingVillagers(livingEntity)
+                || persistentDataLayer.hasMobFlag(MobMetaFlagType.EGG_MOB, livingEntity)
+                || persistentDataLayer.hasMobFlag(MobMetaFlagType.MOB_SPAWNER_MOB, livingEntity)) {
             return;
         }
 
-        entity.setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
+        persistentDataLayer.flagMetadata(MobMetaFlagType.PLAYER_TAMED_MOB, livingEntity);
 
         //Profile not loaded
         if(UserManager.getPlayer(player) == null)
@@ -997,7 +965,7 @@ public class EntityListener implements Listener {
             return;
         }
 
-        UserManager.getPlayer(player).getTamingManager().awardTamingXP(entity);
+        UserManager.getPlayer(player).getTamingManager().awardTamingXP(livingEntity);
     }
 
     /**
@@ -1069,15 +1037,4 @@ public class EntityListener implements Listener {
             }
         }
     }
-    
-    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
-    public void onPigZapEvent(PigZapEvent event) {
-        /* WORLD BLACKLIST CHECK */
-        if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
-            return;
-
-        if (event.getEntity().hasMetadata(mcMMO.entityMetadataKey)) {
-            event.getPigZombie().setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
-        }
-    }
 }

+ 8 - 6
src/main/java/com/gmail/nossr50/mcMMO.java

@@ -81,6 +81,7 @@ public class mcMMO extends JavaPlugin {
     private static MaterialMapStore materialMapStore;
     private static PlayerLevelUtils playerLevelUtils;
     private static SmeltingTracker smeltingTracker;
+    private static TransientMetadataTools transientMetadataTools;
 
     /* Adventure */
     private static BukkitAudiences audiences;
@@ -120,11 +121,9 @@ public class mcMMO extends JavaPlugin {
     public static final String FISH_HOOK_REF_METAKEY = "mcMMO: Fish Hook Tracker";
     public static final String DODGE_TRACKER        = "mcMMO: Dodge Tracker";
     public static final String CUSTOM_DAMAGE_METAKEY = "mcMMO: Custom Damage";
-    public static final String COTW_TEMPORARY_SUMMON = "mcMMO: COTW Entity";
-    public final static String entityMetadataKey   = "mcMMO: Spawned Entity";
+    public final static String travelingBlock      = "mcMMO: Traveling Block";
     public final static String blockMetadataKey    = "mcMMO: Piston Tracking";
     public final static String tntMetadataKey      = "mcMMO: Tracked TNT";
-    public final static String funfettiMetadataKey = "mcMMO: Funfetti";
     public final static String customNameKey       = "mcMMO: Custom Name";
     public final static String customVisibleKey    = "mcMMO: Name Visibility";
     public final static String droppedItemKey      = "mcMMO: Tracked Item";
@@ -133,12 +132,9 @@ public class mcMMO extends JavaPlugin {
     public final static String bowForceKey         = "mcMMO: Bow Force";
     public final static String arrowDistanceKey    = "mcMMO: Arrow Distance";
     public final static String BONUS_DROPS_METAKEY = "mcMMO: Double Drops";
-    //public final static String customDamageKey     = "mcMMO: Custom Damage";
     public final static String disarmedItemKey     = "mcMMO: Disarmed Item";
     public final static String playerDataKey       = "mcMMO: Player Data";
-    public final static String greenThumbDataKey   = "mcMMO: Green Thumb";
     public final static String databaseCommandKey  = "mcMMO: Processing Database Command";
-    public final static String bredMetadataKey     = "mcMMO: Bred Animal";
 
     public static FixedMetadataValue metadataValue;
 
@@ -282,6 +278,8 @@ public class mcMMO extends JavaPlugin {
         smeltingTracker = new SmeltingTracker();
 
         audiences = BukkitAudiences.create(this);
+
+        transientMetadataTools = new TransientMetadataTools(this);
     }
 
     public static PlayerLevelUtils getPlayerLevelUtils() {
@@ -699,4 +697,8 @@ public class mcMMO extends JavaPlugin {
     public static boolean isProjectKorraEnabled() {
         return projectKorraEnabled;
     }
+
+    public static TransientMetadataTools getTransientMetadataTools() {
+        return transientMetadataTools;
+    }
 }

+ 3 - 5
src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java

@@ -17,6 +17,7 @@ import com.gmail.nossr50.skills.SkillManager;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.StringUtils;
+import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.random.RandomChanceSkillStatic;
 import com.gmail.nossr50.util.random.RandomChanceUtil;
@@ -485,12 +486,9 @@ public class TamingManager extends SkillManager {
         callOfWildEntity.setRemoveWhenFarAway(false);
     }
 
-    private void applyMetaDataToCOTWEntity(LivingEntity callOfWildEntity) {
-        //This is used to prevent XP gains for damaging this entity
-        callOfWildEntity.setMetadata(mcMMO.entityMetadataKey, mcMMO.metadataValue);
-
+    private void applyMetaDataToCOTWEntity(LivingEntity summonedEntity) {
         //This helps identify the entity as being summoned by COTW
-        callOfWildEntity.setMetadata(mcMMO.COTW_TEMPORARY_SUMMON, mcMMO.metadataValue);
+        mcMMO.getCompatibilityManager().getPersistentDataLayer().flagMetadata(MobMetaFlagType.COTW_SUMMONED_MOB, summonedEntity);
     }
 
     /**

+ 37 - 0
src/main/java/com/gmail/nossr50/util/TransientMetadataTools.java

@@ -0,0 +1,37 @@
+package com.gmail.nossr50.util;
+
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.compat.layers.persistentdata.SpigotPersistentDataLayer_1_13;
+import org.bukkit.entity.LivingEntity;
+
+public class TransientMetadataTools {
+    private final mcMMO pluginRef;
+
+    public TransientMetadataTools(mcMMO pluginRef) {
+        this.pluginRef = pluginRef;
+    }
+
+    public void cleanAllMobMetadata(LivingEntity livingEntity) {
+        //Since its not written anywhere, apparently the GC won't touch objects with metadata still present on them
+        if (livingEntity.hasMetadata(mcMMO.customNameKey)) {
+            livingEntity.setCustomName(livingEntity.getMetadata(mcMMO.customNameKey).get(0).asString());
+            livingEntity.removeMetadata(mcMMO.customNameKey, pluginRef);
+        }
+
+        //Involved in changing mob names to hearts
+        if (livingEntity.hasMetadata(mcMMO.customVisibleKey)) {
+            livingEntity.setCustomNameVisible(livingEntity.getMetadata(mcMMO.customVisibleKey).get(0).asBoolean());
+            livingEntity.removeMetadata(mcMMO.customVisibleKey, pluginRef);
+        }
+
+        //Gets assigned to endermen, potentially doesn't get cleared before this point
+        if(livingEntity.hasMetadata(mcMMO.travelingBlock)) {
+            livingEntity.removeMetadata(mcMMO.travelingBlock, pluginRef);
+        }
+
+        //1.13.2 uses transient mob flags and needs to be cleaned up
+        if(mcMMO.getCompatibilityManager().getPersistentDataLayer() instanceof SpigotPersistentDataLayer_1_13) {
+            mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity);
+        }
+    }
+}

+ 105 - 9
src/main/java/com/gmail/nossr50/util/compat/layers/persistentdata/AbstractPersistentDataLayer.java

@@ -1,32 +1,128 @@
 package com.gmail.nossr50.util.compat.layers.persistentdata;
 
+import com.gmail.nossr50.api.exceptions.IncompleteNamespacedKeyRegister;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.compat.layers.AbstractCompatibilityLayer;
 import org.bukkit.NamespacedKey;
 import org.bukkit.block.Furnace;
+import org.bukkit.entity.LivingEntity;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
 
 public abstract class AbstractPersistentDataLayer extends AbstractCompatibilityLayer {
 
-    public static final String LEGACY_ABILITY_TOOL_LORE = "mcMMO Ability Tool";
-    public final NamespacedKey superAbilityBoosted;
-    public final String SUPER_ABILITY_BOOSTED = "super_ability_boosted";
+    protected final @NotNull NamespacedKey NSK_SUPER_ABILITY_BOOSTED_ITEM;
+    protected final @NotNull NamespacedKey NSK_MOB_SPAWNER_MOB;
+    protected final @NotNull NamespacedKey NSK_EGG_MOB;
+    protected final @NotNull NamespacedKey NSK_NETHER_GATE_MOB;
+    protected final @NotNull NamespacedKey NSK_COTW_SUMMONED_MOB;
+    protected final @NotNull NamespacedKey NSK_PLAYER_BRED_MOB;
+    protected final @NotNull NamespacedKey NSK_PLAYER_TAMED_MOB;
+    protected final @NotNull NamespacedKey NSK_VILLAGER_TRADE_ORIGIN_ITEM;
+    protected final @NotNull NamespacedKey NSK_EXPLOITED_ENDERMEN;
+
+    //Never change these constants
+    public final @NotNull String STR_SUPER_ABILITY_BOOSTED_ITEM = "super_ability_boosted";
+    public final @NotNull String STR_MOB_SPAWNER_MOB = "mcmmo_mob_spawner_mob";
+    public final @NotNull String STR_EGG_MOB = "mcmmo_egg_mob";
+    public final @NotNull String STR_NETHER_PORTAL_MOB = "mcmmo_nethergate_mob";
+    public final @NotNull String STR_COTW_SUMMONED_MOB = "mcmmo_cotw_summoned_mob";
+    public final @NotNull String STR_PLAYER_BRED_MOB = "mcmmo_player_bred_mob";
+    public final @NotNull String STR_PLAYER_TAMED_MOB = "mcmmo_player_tamed_mob";
+    public final @NotNull String STR_VILLAGER_TRADE_ORIGIN_ITEM = "mcmmo_villager_trade_origin_item";
+    public final @NotNull String STR_EXPLOITED_ENDERMEN = "mcmmo_exploited_endermen";
+
+    /*
+     * Don't modify these keys
+     */
+    public final @NotNull String STR_FURNACE_UUID_MOST_SIG = "furnace_uuid_most_sig";
+    public final @NotNull String STR_FURNACE_UUID_LEAST_SIG = "furnace_uuid_least_sig";
+
+    protected final @NotNull NamespacedKey NSK_FURNACE_UUID_MOST_SIG;
+    protected final @NotNull NamespacedKey NSK_FURNACE_UUID_LEAST_SIG;
+
+    public final @NotNull String LEGACY_ABILITY_TOOL_LORE = "mcMMO Ability Tool";
+
+    protected static final byte SIMPLE_FLAG_VALUE = (byte) 0x1;
 
     public AbstractPersistentDataLayer() {
-        superAbilityBoosted = getNamespacedKey(SUPER_ABILITY_BOOSTED);
+        NSK_SUPER_ABILITY_BOOSTED_ITEM = getNamespacedKey(STR_SUPER_ABILITY_BOOSTED_ITEM);
+        NSK_MOB_SPAWNER_MOB = getNamespacedKey(STR_MOB_SPAWNER_MOB);
+        NSK_EGG_MOB = getNamespacedKey(STR_EGG_MOB);
+        NSK_NETHER_GATE_MOB = getNamespacedKey(STR_NETHER_PORTAL_MOB);
+        NSK_COTW_SUMMONED_MOB = getNamespacedKey(STR_COTW_SUMMONED_MOB);
+        NSK_PLAYER_BRED_MOB = getNamespacedKey(STR_PLAYER_BRED_MOB);
+        NSK_PLAYER_TAMED_MOB = getNamespacedKey(STR_PLAYER_TAMED_MOB);
+        NSK_VILLAGER_TRADE_ORIGIN_ITEM = getNamespacedKey(STR_VILLAGER_TRADE_ORIGIN_ITEM);
+        NSK_EXPLOITED_ENDERMEN = getNamespacedKey(STR_EXPLOITED_ENDERMEN);
+        NSK_FURNACE_UUID_MOST_SIG = getNamespacedKey(STR_FURNACE_UUID_MOST_SIG);
+        NSK_FURNACE_UUID_LEAST_SIG = getNamespacedKey(STR_FURNACE_UUID_LEAST_SIG);
+
         initializeLayer();
     }
 
-    public @NotNull NamespacedKey getNamespacedKey(@NotNull String key) {
+
+    /**
+     * Helper method to simplify generating namespaced keys
+     * @param key the {@link String} value of the key
+     * @return the generated {@link NamespacedKey}
+     */
+    private @NotNull NamespacedKey getNamespacedKey(@NotNull String key) {
         return new NamespacedKey(mcMMO.p, key);
     }
 
+    /**
+     * Whether or not a target {@link LivingEntity} has a specific mcMMO mob flags
+     * @param flag the type of mob flag to check for
+     * @param livingEntity the living entity to check for metadata
+     * @return true if the mob has metadata values for target {@link MobMetaFlagType}
+     */
+    public abstract boolean hasMobFlag(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity);
+
+    /**
+     * Whether or not a target {@link LivingEntity} has any mcMMO mob flags
+     * @param livingEntity the living entity to check for metadata
+     * @return true if the mob has any mcMMO mob related metadata values
+     */
+    public abstract boolean hasMobFlags(@NotNull LivingEntity livingEntity);
+
+    /**
+     * Copies all mcMMO mob flags from one {@link LivingEntity} to another {@link LivingEntity}
+     * This does not clear existing mcMMO mob flags on the target
+     * @param sourceEntity entity to copy from
+     * @param targetEntity entity to copy to
+     */
+    public abstract void addMobFlags(@NotNull LivingEntity sourceEntity, @NotNull LivingEntity targetEntity);
+
+    /**
+     * Adds a mob flag to a {@link LivingEntity} which effectively acts a true/false boolean
+     * Existence of the flag can be considered a true value, non-existence can be considered false for all intents and purposes
+     * @param flag the desired flag to assign
+     * @param livingEntity the target living entity
+     */
+    public abstract void flagMetadata(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity);
+
+    /**
+     * Removes a specific mob flag from target {@link LivingEntity}
+     * @param flag desired flag to remove
+     * @param livingEntity the target living entity
+     */
+    public abstract void removeMobFlag(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity);
+
+    /**
+     * Remove all mcMMO related mob flags from the target {@link LivingEntity}
+     * @param livingEntity target entity
+     */
+    public void removeMobFlags(@NotNull LivingEntity livingEntity) {
+        for(MobMetaFlagType flag : MobMetaFlagType.values()) {
+            removeMobFlag(flag, livingEntity);
+        }
+    }
+
     public abstract @Nullable UUID getFurnaceOwner(@NotNull Furnace furnace);
 
     public abstract void setFurnaceOwner(@NotNull Furnace furnace, @NotNull UUID uuid);
@@ -39,7 +135,7 @@ public abstract class AbstractPersistentDataLayer extends AbstractCompatibilityL
 
     public abstract void removeBonusDigSpeedOnSuperAbilityTool(@NotNull ItemStack itemStack);
 
-    public boolean isLegacyAbilityTool(ItemStack itemStack) {
+    public boolean isLegacyAbilityTool(@NotNull ItemStack itemStack) {
         ItemMeta itemMeta = itemStack.getItemMeta();
 
         if(itemMeta == null)
@@ -53,7 +149,7 @@ public abstract class AbstractPersistentDataLayer extends AbstractCompatibilityL
         return lore.contains(LEGACY_ABILITY_TOOL_LORE);
     }
 
-    public static String getLegacyAbilityToolLore() {
+    public @NotNull String getLegacyAbilityToolLore() {
         return LEGACY_ABILITY_TOOL_LORE;
     }
 }

+ 11 - 0
src/main/java/com/gmail/nossr50/util/compat/layers/persistentdata/MobMetaFlagType.java

@@ -0,0 +1,11 @@
+package com.gmail.nossr50.util.compat.layers.persistentdata;
+
+public enum MobMetaFlagType {
+    MOB_SPAWNER_MOB,
+    EGG_MOB,
+    NETHER_PORTAL_MOB,
+    COTW_SUMMONED_MOB,
+    PLAYER_BRED_MOB,
+    PLAYER_TAMED_MOB,
+    EXPLOITED_ENDERMEN,
+}

+ 88 - 10
src/main/java/com/gmail/nossr50/util/compat/layers/persistentdata/SpigotPersistentDataLayer_1_13.java

@@ -1,9 +1,11 @@
 package com.gmail.nossr50.util.compat.layers.persistentdata;
 
+import com.gmail.nossr50.api.exceptions.IncompleteNamespacedKeyRegister;
 import com.gmail.nossr50.datatypes.meta.UUIDMeta;
 import com.gmail.nossr50.mcMMO;
 import org.bukkit.block.Furnace;
 import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.LivingEntity;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.inventory.meta.tags.CustomItemTagContainer;
@@ -11,6 +13,7 @@ import org.bukkit.inventory.meta.tags.ItemTagType;
 import org.bukkit.metadata.Metadatable;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.EnumMap;
 import java.util.UUID;
 
 /**
@@ -18,19 +21,94 @@ import java.util.UUID;
  */
 public class SpigotPersistentDataLayer_1_13 extends AbstractPersistentDataLayer {
 
-    private final String FURNACE_OWNER_METADATA_KEY = "mcMMO_furnace_owner";
+    private final @NotNull String KEY_FURNACE_OWNER = "mcMMO_furnace_owner";
+    private final @NotNull EnumMap<MobMetaFlagType, String> mobFlagKeyMap;
+
+    public SpigotPersistentDataLayer_1_13() {
+        mobFlagKeyMap = new EnumMap<>(MobMetaFlagType.class);
+        initMobFlagKeyMap();
+    }
 
     @Override
     public boolean initializeLayer() {
         return true;
     }
 
+    private void initMobFlagKeyMap() throws IncompleteNamespacedKeyRegister {
+        for(MobMetaFlagType flagType : MobMetaFlagType.values()) {
+            switch(flagType) {
+                case MOB_SPAWNER_MOB:
+                    mobFlagKeyMap.put(flagType, STR_MOB_SPAWNER_MOB);
+                    break;
+                case EGG_MOB:
+                    mobFlagKeyMap.put(flagType, STR_EGG_MOB);
+                    break;
+                case NETHER_PORTAL_MOB:
+                    mobFlagKeyMap.put(flagType, STR_NETHER_PORTAL_MOB);
+                    break;
+                case COTW_SUMMONED_MOB:
+                    mobFlagKeyMap.put(flagType, STR_COTW_SUMMONED_MOB);
+                    break;
+                case PLAYER_BRED_MOB:
+                    mobFlagKeyMap.put(flagType, STR_PLAYER_BRED_MOB);
+                    break;
+                case PLAYER_TAMED_MOB:
+                    mobFlagKeyMap.put(flagType, STR_PLAYER_TAMED_MOB);
+                    break;
+                case EXPLOITED_ENDERMEN:
+                    mobFlagKeyMap.put(flagType, STR_EXPLOITED_ENDERMEN);
+                    break;
+                default:
+                    throw new IncompleteNamespacedKeyRegister("Missing flag register for: "+flagType.toString());
+            }
+        }
+    }
+
+    @Override
+    public boolean hasMobFlag(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity) {
+        return livingEntity.hasMetadata(mobFlagKeyMap.get(flag));
+    }
+
+    @Override
+    public boolean hasMobFlags(@NotNull LivingEntity livingEntity) {
+        for(String currentKey : mobFlagKeyMap.values()) {
+            if(livingEntity.hasMetadata(currentKey)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void addMobFlags(@NotNull LivingEntity sourceEntity, @NotNull LivingEntity targetEntity) {
+        for(MobMetaFlagType flag : MobMetaFlagType.values()) {
+            if(hasMobFlag(flag, sourceEntity)) {
+                flagMetadata(flag, targetEntity);
+            }
+        }
+    }
+
+    @Override
+    public void flagMetadata(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity) {
+        if(!hasMobFlag(flag, livingEntity)) {
+            livingEntity.setMetadata(mobFlagKeyMap.get(flag), mcMMO.metadataValue);
+        }
+    }
+
+    @Override
+    public void removeMobFlag(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity) {
+        if(hasMobFlag(flag, livingEntity)) {
+            livingEntity.removeMetadata(mobFlagKeyMap.get(flag), mcMMO.p);
+        }
+    }
+
     @Override
     public UUID getFurnaceOwner(@NotNull Furnace furnace) {
         Metadatable metadatable = (Metadatable) furnace;
 
-        if(metadatable.getMetadata(FURNACE_OWNER_METADATA_KEY).size() > 0) {
-            UUIDMeta uuidMeta = (UUIDMeta) metadatable.getMetadata(FURNACE_OWNER_METADATA_KEY).get(0);
+        if(metadatable.getMetadata(KEY_FURNACE_OWNER).size() > 0) {
+            UUIDMeta uuidMeta = (UUIDMeta) metadatable.getMetadata(KEY_FURNACE_OWNER).get(0);
             return (UUID) uuidMeta.value();
         } else {
             return null;
@@ -41,11 +119,11 @@ public class SpigotPersistentDataLayer_1_13 extends AbstractPersistentDataLayer
     public void setFurnaceOwner(@NotNull Furnace furnace, @NotNull UUID uuid) {
         Metadatable metadatable = (Metadatable) furnace;
 
-        if(metadatable.getMetadata(FURNACE_OWNER_METADATA_KEY).size() > 0) {
-            metadatable.removeMetadata(FURNACE_OWNER_METADATA_KEY, mcMMO.p);
+        if(metadatable.getMetadata(KEY_FURNACE_OWNER).size() > 0) {
+            metadatable.removeMetadata(KEY_FURNACE_OWNER, mcMMO.p);
         }
 
-        metadatable.setMetadata(FURNACE_OWNER_METADATA_KEY, new UUIDMeta(mcMMO.p, uuid));
+        metadatable.setMetadata(KEY_FURNACE_OWNER, new UUIDMeta(mcMMO.p, uuid));
     }
 
     @Override
@@ -57,7 +135,7 @@ public class SpigotPersistentDataLayer_1_13 extends AbstractPersistentDataLayer
             return;
         }
 
-        itemMeta.getCustomTagContainer().setCustomTag(superAbilityBoosted, ItemTagType.INTEGER, originalDigSpeed);
+        itemMeta.getCustomTagContainer().setCustomTag(NSK_SUPER_ABILITY_BOOSTED_ITEM, ItemTagType.INTEGER, originalDigSpeed);
         itemStack.setItemMeta(itemMeta);
     }
 
@@ -69,7 +147,7 @@ public class SpigotPersistentDataLayer_1_13 extends AbstractPersistentDataLayer
             return false;
 
         CustomItemTagContainer tagContainer = itemMeta.getCustomTagContainer();
-        return tagContainer.hasCustomTag(superAbilityBoosted, ItemTagType.INTEGER);
+        return tagContainer.hasCustomTag(NSK_SUPER_ABILITY_BOOSTED_ITEM, ItemTagType.INTEGER);
     }
 
     @Override
@@ -81,8 +159,8 @@ public class SpigotPersistentDataLayer_1_13 extends AbstractPersistentDataLayer
 
         CustomItemTagContainer tagContainer = itemMeta.getCustomTagContainer();
 
-        if(tagContainer.hasCustomTag(superAbilityBoosted , ItemTagType.INTEGER)) {
-            return tagContainer.getCustomTag(superAbilityBoosted, ItemTagType.INTEGER);
+        if(tagContainer.hasCustomTag(NSK_SUPER_ABILITY_BOOSTED_ITEM, ItemTagType.INTEGER)) {
+            return tagContainer.getCustomTag(NSK_SUPER_ABILITY_BOOSTED_ITEM, ItemTagType.INTEGER);
         } else {
             return 0;
         }

+ 90 - 20
src/main/java/com/gmail/nossr50/util/compat/layers/persistentdata/SpigotPersistentDataLayer_1_14.java

@@ -1,9 +1,11 @@
 package com.gmail.nossr50.util.compat.layers.persistentdata;
 
+import com.gmail.nossr50.api.exceptions.IncompleteNamespacedKeyRegister;
 import com.gmail.nossr50.mcMMO;
 import org.bukkit.NamespacedKey;
 import org.bukkit.block.Furnace;
 import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.LivingEntity;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.persistence.PersistentDataContainer;
@@ -12,28 +14,96 @@ import org.bukkit.persistence.PersistentDataType;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.EnumMap;
 import java.util.UUID;
 
 public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer {
 
-    /*
-     * Don't modify these keys
-     */
-    public static final String FURNACE_UUID_MOST_SIG = "furnace_uuid_most_sig";
-    public static final String FURNACE_UUID_LEAST_SIG = "furnace_uuid_least_sig";
+    private final @NotNull EnumMap<MobMetaFlagType, NamespacedKey> mobFlagKeyMap;
 
-    private NamespacedKey furnaceOwner_MostSig_Key;
-    private NamespacedKey furnaceOwner_LeastSig_Key;
+    public SpigotPersistentDataLayer_1_14() {
+        mobFlagKeyMap = new EnumMap<>(MobMetaFlagType.class);
+        initMobFlagKeyMap();
+    }
 
     @Override
     public boolean initializeLayer() {
-        initNamespacedKeys();
         return true;
     }
 
-    private void initNamespacedKeys() {
-        furnaceOwner_MostSig_Key = getNamespacedKey(FURNACE_UUID_MOST_SIG);
-        furnaceOwner_LeastSig_Key = getNamespacedKey(FURNACE_UUID_LEAST_SIG);
+    /**
+     * Registers the namespaced keys required by the API (CB/Spigot)
+     */
+    private void initMobFlagKeyMap() throws IncompleteNamespacedKeyRegister {
+        for(MobMetaFlagType mobMetaFlagType : MobMetaFlagType.values()) {
+            switch(mobMetaFlagType) {
+
+                case MOB_SPAWNER_MOB:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_MOB_SPAWNER_MOB);
+                    break;
+                case EGG_MOB:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_EGG_MOB);
+                    break;
+                case NETHER_PORTAL_MOB:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_NETHER_GATE_MOB);
+                    break;
+                case COTW_SUMMONED_MOB:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_COTW_SUMMONED_MOB);
+                    break;
+                case PLAYER_BRED_MOB:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_PLAYER_BRED_MOB);
+                    break;
+                case EXPLOITED_ENDERMEN:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_EXPLOITED_ENDERMEN);
+                    break;
+                case PLAYER_TAMED_MOB:
+                    mobFlagKeyMap.put(mobMetaFlagType, NSK_PLAYER_TAMED_MOB);
+                    break;
+                default:
+                    throw new IncompleteNamespacedKeyRegister("missing namespaced key register for type: "+ mobMetaFlagType.toString());
+            }
+        }
+    }
+
+    @Override
+    public boolean hasMobFlag(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity) {
+        return livingEntity.getPersistentDataContainer().has(mobFlagKeyMap.get(flag), PersistentDataType.SHORT);
+    }
+
+    @Override
+    public boolean hasMobFlags(@NotNull LivingEntity livingEntity) {
+        for(NamespacedKey currentKey : mobFlagKeyMap.values()) {
+            if(livingEntity.getPersistentDataContainer().has(currentKey, PersistentDataType.BYTE)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void addMobFlags(@NotNull LivingEntity sourceEntity, @NotNull LivingEntity targetEntity) {
+        for(MobMetaFlagType flag : MobMetaFlagType.values()) {
+            if(hasMobFlag(flag, sourceEntity)) {
+                flagMetadata(flag, targetEntity);
+            }
+        }
+    }
+
+    @Override
+    public void flagMetadata(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity) {
+        if(!hasMobFlag(flag, livingEntity)) {
+            PersistentDataContainer persistentDataContainer = livingEntity.getPersistentDataContainer();
+            persistentDataContainer.set(mobFlagKeyMap.get(flag), PersistentDataType.BYTE, SIMPLE_FLAG_VALUE);
+        }
+    }
+
+    @Override
+    public void removeMobFlag(@NotNull MobMetaFlagType flag, @NotNull LivingEntity livingEntity) {
+        if(hasMobFlag(flag, livingEntity)) {
+            PersistentDataContainer persistentDataContainer = livingEntity.getPersistentDataContainer();
+            persistentDataContainer.remove(mobFlagKeyMap.get(flag));
+        }
     }
 
     @Override
@@ -42,8 +112,8 @@ public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer
         PersistentDataContainer dataContainer = ((PersistentDataHolder) furnace).getPersistentDataContainer();
 
         //Too lazy to make a custom data type for this stuff
-        Long mostSigBits = dataContainer.get(furnaceOwner_MostSig_Key, PersistentDataType.LONG);
-        Long leastSigBits = dataContainer.get(furnaceOwner_LeastSig_Key, PersistentDataType.LONG);
+        Long mostSigBits = dataContainer.get(NSK_FURNACE_UUID_MOST_SIG, PersistentDataType.LONG);
+        Long leastSigBits = dataContainer.get(NSK_FURNACE_UUID_LEAST_SIG, PersistentDataType.LONG);
 
         if(mostSigBits != null && leastSigBits != null) {
             return new UUID(mostSigBits, leastSigBits);
@@ -56,8 +126,8 @@ public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer
     public void setFurnaceOwner(@NotNull Furnace furnace, @NotNull UUID uuid) {
         PersistentDataContainer dataContainer = ((PersistentDataHolder) furnace).getPersistentDataContainer();
 
-        dataContainer.set(furnaceOwner_MostSig_Key, PersistentDataType.LONG, uuid.getMostSignificantBits());
-        dataContainer.set(furnaceOwner_LeastSig_Key, PersistentDataType.LONG, uuid.getLeastSignificantBits());
+        dataContainer.set(NSK_FURNACE_UUID_MOST_SIG, PersistentDataType.LONG, uuid.getMostSignificantBits());
+        dataContainer.set(NSK_FURNACE_UUID_LEAST_SIG, PersistentDataType.LONG, uuid.getLeastSignificantBits());
 
         furnace.update();
     }
@@ -72,7 +142,7 @@ public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer
         ItemMeta itemMeta = itemStack.getItemMeta();
         PersistentDataContainer dataContainer = itemMeta.getPersistentDataContainer();
 
-        dataContainer.set(superAbilityBoosted, PersistentDataType.INTEGER, originalDigSpeed);
+        dataContainer.set(NSK_SUPER_ABILITY_BOOSTED_ITEM, PersistentDataType.INTEGER, originalDigSpeed);
 
         itemStack.setItemMeta(itemMeta);
     }
@@ -87,7 +157,7 @@ public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer
         PersistentDataContainer dataContainer = itemMeta.getPersistentDataContainer();
 
         //If this value isn't null, then the tool can be considered dig speed boosted
-        Integer boostValue = dataContainer.get(superAbilityBoosted, PersistentDataType.INTEGER);
+        Integer boostValue = dataContainer.get(NSK_SUPER_ABILITY_BOOSTED_ITEM, PersistentDataType.INTEGER);
 
         return boostValue != null;
     }
@@ -102,12 +172,12 @@ public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer
 
         PersistentDataContainer dataContainer = itemMeta.getPersistentDataContainer();
 
-        if(dataContainer.get(superAbilityBoosted, PersistentDataType.INTEGER) == null) {
+        if(dataContainer.get(NSK_SUPER_ABILITY_BOOSTED_ITEM, PersistentDataType.INTEGER) == null) {
             mcMMO.p.getLogger().severe("Value should never be null for a boosted item");
             return 0;
         } else {
             //Too lazy to make a custom data type for this stuff
-            Integer boostValue = dataContainer.get(superAbilityBoosted, PersistentDataType.INTEGER);
+            Integer boostValue = dataContainer.get(NSK_SUPER_ABILITY_BOOSTED_ITEM, PersistentDataType.INTEGER);
             return Math.max(boostValue, 0);
         }
     }
@@ -127,7 +197,7 @@ public class SpigotPersistentDataLayer_1_14 extends AbstractPersistentDataLayer
         }
 
         PersistentDataContainer dataContainer = itemMeta.getPersistentDataContainer();
-        dataContainer.remove(superAbilityBoosted); //Remove persistent data
+        dataContainer.remove(NSK_SUPER_ABILITY_BOOSTED_ITEM); //Remove persistent data
 
         //TODO: needed?
         itemStack.setItemMeta(itemMeta);

+ 66 - 27
src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java

@@ -20,6 +20,8 @@ import com.gmail.nossr50.skills.swords.SwordsManager;
 import com.gmail.nossr50.skills.taming.TamingManager;
 import com.gmail.nossr50.skills.unarmed.UnarmedManager;
 import com.gmail.nossr50.util.*;
+import com.gmail.nossr50.util.compat.layers.persistentdata.AbstractPersistentDataLayer;
+import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.UserManager;
 import com.google.common.collect.ImmutableMap;
@@ -41,8 +43,13 @@ import java.util.List;
 import java.util.Map;
 
 public final class CombatUtils {
+
     private CombatUtils() {}
 
+    private static AbstractPersistentDataLayer getPersistentData() {
+        return mcMMO.getCompatibilityManager().getPersistentDataLayer();
+    }
+
     //Likely.. because who knows what plugins are throwing around
     public static boolean isDamageLikelyFromNormalCombat(DamageCause damageCause) {
         switch (damageCause) {
@@ -106,6 +113,14 @@ public final class CombatUtils {
 
         applyScaledModifiers(initialDamage, finalDamage, event);
         processCombatXP(mcMMOPlayer, target, PrimarySkillType.SWORDS);
+
+        printFinalDamageDebug(player, event, mcMMOPlayer);
+    }
+
+    private static void printFinalDamageDebug(Player player, EntityDamageByEntityEvent event, McMMOPlayer mcMMOPlayer) {
+        if(mcMMOPlayer.isDebugMode()) {
+            player.sendMessage("Final Damage value after mcMMO modifiers: "+ event.getFinalDamage());
+        }
     }
 
 //    public static void strengthDebug(Player player) {
@@ -181,6 +196,8 @@ public final class CombatUtils {
 
         applyScaledModifiers(initialDamage, finalDamage, event);
         processCombatXP(mcMMOPlayer, target, PrimarySkillType.AXES);
+
+        printFinalDamageDebug(player, event, mcMMOPlayer);
     }
 
     private static void processUnarmedCombat(LivingEntity target, Player player, EntityDamageByEntityEvent event) {
@@ -223,6 +240,8 @@ public final class CombatUtils {
 
         applyScaledModifiers(initialDamage, finalDamage, event);
         processCombatXP(mcMMOPlayer, target, PrimarySkillType.UNARMED);
+
+        printFinalDamageDebug(player, event, mcMMOPlayer);
     }
 
     private static void processTamingCombat(LivingEntity target, Player master, Wolf wolf, EntityDamageByEntityEvent event) {
@@ -299,6 +318,8 @@ public final class CombatUtils {
 
         applyScaledModifiers(initialDamage, finalDamage, event);
         processCombatXP(mcMMOPlayer, target, PrimarySkillType.ARCHERY, forceMultiplier * distanceMultiplier);
+
+        printFinalDamageDebug(player, event, mcMMOPlayer);
     }
 
     /**
@@ -372,6 +393,7 @@ public final class CombatUtils {
 
                 if (PrimarySkillType.SWORDS.getPermissions(player)) {
                     processSwordCombat(target, player, event);
+
                 }
             }
             else if (ItemUtils.isAxe(heldItem)) {
@@ -428,6 +450,7 @@ public final class CombatUtils {
                 }
             }
         }
+
     }
 
     /**
@@ -547,21 +570,21 @@ public final class CombatUtils {
         dealDamage(target, damage, DamageCause.CUSTOM, attacker);
     }
 
-    /**
-     * Attempt to damage target for value dmg with reason ENTITY_ATTACK with damager attacker
-     *
-     * @param target LivingEntity which to attempt to damage
-     * @param damage Amount of damage to attempt to do
-     * @param attacker Player to pass to event as damager
-     */
-    public static void dealDamage(LivingEntity target, double damage, Map<DamageModifier, Double> modifiers, LivingEntity attacker) {
-        if (target.isDead()) {
-            return;
-        }
-
-        // Aren't we applying the damage twice????
-        target.damage(getFakeDamageFinalResult(attacker, target, damage, modifiers));
-    }
+//    /**
+//     * Attempt to damage target for value dmg with reason ENTITY_ATTACK with damager attacker
+//     *
+//     * @param target LivingEntity which to attempt to damage
+//     * @param damage Amount of damage to attempt to do
+//     * @param attacker Player to pass to event as damager
+//     */
+//    public static void dealDamage(LivingEntity target, double damage, Map<DamageModifier, Double> modifiers, LivingEntity attacker) {
+//        if (target.isDead()) {
+//            return;
+//        }
+//
+//        // Aren't we applying the damage twice????
+//        target.damage(getFakeDamageFinalResult(attacker, target, damage, modifiers));
+//    }
 
     /**
      * Attempt to damage target for value dmg with reason ENTITY_ATTACK with damager attacker
@@ -576,8 +599,11 @@ public final class CombatUtils {
             return;
         }
 
-        if(canDamage(attacker, target, cause, damage))
+        if(canDamage(attacker, target, cause, damage)) {
+            applyIgnoreDamageMetadata(target);
             target.damage(damage);
+            removeIgnoreDamageMetadata(target);
+        }
     }
 
     private static boolean processingNoInvulnDamage;
@@ -596,21 +622,33 @@ public final class CombatUtils {
         // cause do have issues around plugin observability. This is not a perfect solution, but it appears to be the best one here
         // We also set no damage ticks to 0, to ensure that damage is applied for this case, and reset it back to the original value
         // Snapshot current state so we can pop up properly
-        boolean wasMetaSet = target.getMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY).size() != 0;
+        boolean wasMetaSet = hasIgnoreDamageMetadata(target);
         boolean wasProcessing = processingNoInvulnDamage;
         // set markers
         processingNoInvulnDamage = true;
-        target.setMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY, mcMMO.metadataValue);
+        applyIgnoreDamageMetadata(target);
         int noDamageTicks = target.getNoDamageTicks();
         target.setNoDamageTicks(0);
         target.damage(damage, attacker);
         target.setNoDamageTicks(noDamageTicks);
         if (!wasMetaSet)
-            target.removeMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY, mcMMO.p);
+            removeIgnoreDamageMetadata(target);
         if (!wasProcessing)
             processingNoInvulnDamage = false;
     }
 
+    public static void removeIgnoreDamageMetadata(LivingEntity target) {
+        target.removeMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY, mcMMO.p);
+    }
+
+    public static void applyIgnoreDamageMetadata(LivingEntity target) {
+        target.setMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY, mcMMO.metadataValue);
+    }
+
+    public static boolean hasIgnoreDamageMetadata(LivingEntity target) {
+        return target.getMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY).size() != 0;
+    }
+
     public static void dealNoInvulnerabilityTickDamageRupture(LivingEntity target, double damage, Entity attacker, int toolTier) {
         if (target.isDead()) {
             return;
@@ -767,19 +805,20 @@ public final class CombatUtils {
                 }
             }
 
-            if (target.hasMetadata(mcMMO.entityMetadataKey)
-                    //Epic Spawners compatibility
-                    || target.hasMetadata("ES")) {
+            if(getPersistentData().hasMobFlag(MobMetaFlagType.COTW_SUMMONED_MOB, target)) {
+                baseXP = 0;
+            } else if(getPersistentData().hasMobFlag(MobMetaFlagType.MOB_SPAWNER_MOB, target) || target.hasMetadata("ES")) {
                 baseXP *= ExperienceConfig.getInstance().getSpawnedMobXpMultiplier();
-            }
-
-            if (target.hasMetadata(mcMMO.bredMetadataKey)) {
+            } else if(getPersistentData().hasMobFlag(MobMetaFlagType.NETHER_PORTAL_MOB, target)) {
+                baseXP *= ExperienceConfig.getInstance().getNetherPortalXpMultiplier();
+            } else if(getPersistentData().hasMobFlag(MobMetaFlagType.EGG_MOB, target)) {
+                baseXP *= ExperienceConfig.getInstance().getEggXpMultiplier();
+            } else if (getPersistentData().hasMobFlag(MobMetaFlagType.PLAYER_BRED_MOB, target)) {
                 baseXP *= ExperienceConfig.getInstance().getBredMobXpMultiplier();
             }
 
-            xpGainReason = XPGainReason.PVE;
-
             baseXP *= 10;
+            xpGainReason = XPGainReason.PVE;
         }
 
         baseXP *= multiplier;

+ 4 - 0
src/main/resources/experience.yml

@@ -139,8 +139,12 @@ Experience_Formula:
         PVP: 1.0
 
     # Experience gained from mobs not naturally spawned will get multiplied by this value. 0 by default.
+    Eggs:
+        Multiplier: 0
     Mobspawners:
         Multiplier: 0
+    Nether_Portal:
+        Multiplier: 0
     Breeding:
         Multiplier: 1.0