Browse Source

Merge branch 'master' of github.com:mcMMO-Dev/mcMMO into tridentsxbows

nossr50 4 years ago
parent
commit
4e8262d818
43 changed files with 1045 additions and 606 deletions
  1. 37 2
      Changelog.txt
  2. 4 0
      pom.xml
  3. 50 0
      src/main/java/com/gmail/nossr50/events/experience/McMMOPlayerPreXpGainEvent.java
  4. 49 0
      src/main/java/com/gmail/nossr50/events/skills/SkillActivationPerkEvent.java
  5. 9 3
      src/main/java/com/gmail/nossr50/listeners/BlockListener.java
  6. 5 15
      src/main/java/com/gmail/nossr50/listeners/ChunkListener.java
  7. 8 3
      src/main/java/com/gmail/nossr50/listeners/EntityListener.java
  8. 27 10
      src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  9. 27 6
      src/main/java/com/gmail/nossr50/mcMMO.java
  10. 3 25
      src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java
  11. 5 77
      src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java
  12. 11 32
      src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java
  13. 5 2
      src/main/java/com/gmail/nossr50/util/BlockUtils.java
  14. 27 0
      src/main/java/com/gmail/nossr50/util/ItemUtils.java
  15. 199 2
      src/main/java/com/gmail/nossr50/util/MaterialMapStore.java
  16. 316 0
      src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java
  17. 111 78
      src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java
  18. 4 120
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
  19. 2 1
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
  20. 2 2
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
  21. 34 116
      src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
  22. 7 4
      src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java
  23. 9 31
      src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
  24. 56 0
      src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java
  25. 7 1
      src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java
  26. 0 44
      src/main/resources/fishing_treasures.yml
  27. 0 1
      src/main/resources/locale/locale_cs_CZ.properties
  28. 0 1
      src/main/resources/locale/locale_de.properties
  29. 2 1
      src/main/resources/locale/locale_en_US.properties
  30. 0 1
      src/main/resources/locale/locale_es.properties
  31. 0 1
      src/main/resources/locale/locale_fr.properties
  32. 0 1
      src/main/resources/locale/locale_hu_HU.properties
  33. 0 1
      src/main/resources/locale/locale_it.properties
  34. 0 1
      src/main/resources/locale/locale_ja_JP.properties
  35. 0 1
      src/main/resources/locale/locale_ko.properties
  36. 0 1
      src/main/resources/locale/locale_lt_LT.properties
  37. 0 1
      src/main/resources/locale/locale_nl.properties
  38. 0 1
      src/main/resources/locale/locale_pl.properties
  39. 0 1
      src/main/resources/locale/locale_ru.properties
  40. 0 1
      src/main/resources/locale/locale_th_TH.properties
  41. 0 1
      src/main/resources/locale/locale_zh_CN.properties
  42. 0 1
      src/main/resources/locale/locale_zh_TW.properties
  43. 29 16
      src/test/java/ChunkStoreTest.java

+ 37 - 2
Changelog.txt

@@ -99,15 +99,50 @@ Version 2.2.000
     Parties got unnecessarily complex in my absence, I have removed many party features in order to simplify parties and bring them closer to my vision. I have also added new features which should improve parties where it matters.
     Parties got unnecessarily complex in my absence, I have removed many party features in order to simplify parties and bring them closer to my vision. I have also added new features which should improve parties where it matters.
     About the removed party features, all the features I removed I consider poor quality features and I don't think they belong in mcMMO. Feel free to yell at me in discord if you disagree.
     About the removed party features, all the features I removed I consider poor quality features and I don't think they belong in mcMMO. Feel free to yell at me in discord if you disagree.
     I don't know what genius decided to make parties public by default, when I found out that parties had been changed to such a system I could barely contain my disgust. Parties are back to being private, you get invited by a party leader or party officer. That is the only way to join a party.
     I don't know what genius decided to make parties public by default, when I found out that parties had been changed to such a system I could barely contain my disgust. Parties are back to being private, you get invited by a party leader or party officer. That is the only way to join a party.
+Version 2.1.168
+    Fixed an IndexOutOfBoundsException error when trying to access UserBlockTracker from an invalid range (thanks t00thpick1)
+    (API) UserBlockTracker is now the interface by which our block-tracker will be known (thanks t00thpick1)
+
+Version 2.1.167
+    Fixed a serious dupe bug
+    Add McMMOPlayerPreXpGainEvent event for plugins to modify given exp (thanks electronicboy)
+    Add SkillActivationPerkEvent (thanks electronicboy)
+
+    NOTE:
+    This bug was introduced in 2.1.165, update immediately if you are on 2.1.165 or 2.1.166
+
+Version 2.1.166
+    Fixed a small memory leak in the new COTW tracker
+    Potentially fixed a ConcurrentModificationException involving the TransientEntityTracker (report this error if you encounter it)
+    Music discs removed from the default fishing_treasures.yml
+    Optimized how mcMMO saves player data (should improve timings on servers with bad disk speeds and or bad connectivity to their SQL server instance)
+
+    NOTES:
+    No one likes fishing up music discs, if you want this change it is recommended you delete fishing_treasures.yml and let it regenerate
+    (You won't have this file if you haven't updated in a while, if so you don't need to do anything)
+    If any of you encounter a ConcurrentModificationException error that mentions TransientEntityTracker in its stack trace after this update let me know, I have another fix in mind for this if this update didn't fix it.
+
 Version 2.1.165
 Version 2.1.165
+    Fixed a bug where Enchanted Books dropped by mcMMO (in Fishing) did not function correctly
     The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)
     The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)
+    Optimized our ChunkUnloadEvent, this should improve timings in this area
+    How mcMMO tracks COTW entities has been rewritten
+    When COTW summons are killed players are now informed (from anything other than the time expiring).
     mcMMO will now be compatible with changes to world height (1.17 compatibility)
     mcMMO will now be compatible with changes to world height (1.17 compatibility)
+    mcMMO will ignore EntityPickupItemEvents from "Fake-Player" NPCs if it recognizes them as such, this will prevent some compatibility issues with some plugins
+    SuperBreaker will now always activate if the target blocks fastest tool is a Pickaxe (used to require the block giving XP or being considered an ore)
+    Master Angler mentions that it works better with a boat in its hover tip
     Added missing cooldown locale message 'Commands.Database.Cooldown'
     Added missing cooldown locale message 'Commands.Database.Cooldown'
+    Added new locale message 'Taming.Summon.COTW.Removed'
+    Updated locale entry 'Fishing.SubSkill.MasterAngler.Description'
 
 
     NOTES:
     NOTES:
+    Books dropped before this fix will not be usable and should just be chucked in lava, the broken books have blue names, the working books have yellow names.
     t00thpick1 has taken time to rewrite our block meta tracking system to be more efficient, easier to maintain, and support upcoming features such as world height changes
     t00thpick1 has taken time to rewrite our block meta tracking system to be more efficient, easier to maintain, and support upcoming features such as world height changes
-    This new system is compatible with the old one, it will convert old files to the new format as needed.
-    This update shouldn't break anything as the API is the same
+    This new system is compatible with the old one, it will convert old files to the new format as needed. You won't even know it is doing anything.
+    This update shouldn't break anything as the API is the same.
+
+    Alright back to work on T&C unless some major bugs come out of this...
 
 
 Version 2.1.164
 Version 2.1.164
     mcMMO will now let players use vanilla blocks that have interactions (such as the vanilla Anvil) which are assigned as either Repair or Salvage blocks if a player is sneaking (see notes)
     mcMMO will now let players use vanilla blocks that have interactions (such as the vanilla Anvil) which are assigned as either Repair or Salvage blocks if a player is sneaking (see notes)

+ 4 - 0
pom.xml

@@ -124,6 +124,10 @@
                     </artifactSet>
                     </artifactSet>
 <!--                    <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>-->
 <!--                    <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>-->
                     <relocations>
                     <relocations>
+                        <relocation>
+                            <pattern>net.kyori.examination</pattern>
+                            <shadedPattern>com.gmail.nossr50.kyori.examination</shadedPattern>
+                        </relocation>
                         <relocation>
                         <relocation>
                             <pattern>net.kyori.adventure</pattern>
                             <pattern>net.kyori.adventure</pattern>
                             <shadedPattern>com.gmail.nossr50.mcmmo.kyori.adventure</shadedPattern>
                             <shadedPattern>com.gmail.nossr50.mcmmo.kyori.adventure</shadedPattern>

+ 50 - 0
src/main/java/com/gmail/nossr50/events/experience/McMMOPlayerPreXpGainEvent.java

@@ -0,0 +1,50 @@
+package com.gmail.nossr50.events.experience;
+
+import com.gmail.nossr50.datatypes.experience.XPGainReason;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Called when a player gains XP in a skill
+ */
+public class McMMOPlayerPreXpGainEvent extends McMMOPlayerExperienceEvent {
+    private float xpGained;
+
+    @Deprecated
+    public McMMOPlayerPreXpGainEvent(Player player, PrimarySkillType skill, float xpGained) {
+        super(player, skill, XPGainReason.UNKNOWN);
+        this.xpGained = xpGained;
+    }
+
+    public McMMOPlayerPreXpGainEvent(Player player, PrimarySkillType skill, float xpGained, XPGainReason xpGainReason) {
+        super(player, skill, xpGainReason);
+        this.xpGained = xpGained;
+    }
+
+    /**
+     * @return int amount of experience gained in this event
+     */
+    public int getXpGained() {
+        return (int) xpGained;
+    }
+
+    /**
+     * @param xpGained int amount of experience gained in this event
+     */
+    public void setXpGained(int xpGained) {
+        this.xpGained = xpGained;
+    }
+
+    private static final HandlerList handlers = new HandlerList();
+
+    @Override
+    public @NotNull HandlerList getHandlers() {
+        return handlers;
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+}

+ 49 - 0
src/main/java/com/gmail/nossr50/events/skills/SkillActivationPerkEvent.java

@@ -0,0 +1,49 @@
+package com.gmail.nossr50.events.skills;
+
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+public class SkillActivationPerkEvent extends Event {
+
+
+
+
+    private static final HandlerList handlers = new HandlerList();
+    private final Player player;
+    private int ticks;
+    private final int maxTicks;
+
+    public SkillActivationPerkEvent(Player player, int ticks, int maxTicks) {
+
+        this.player = player;
+        this.ticks = ticks;
+        this.maxTicks = maxTicks;
+    }
+
+    public Player getPlayer() {
+        return player;
+    }
+
+    public int getTicks() {
+        return ticks;
+    }
+
+    public void setTicks(int ticks) {
+        this.ticks = ticks;
+    }
+
+    public int getMaxTicks() {
+        return maxTicks;
+    }
+
+    @Override
+    public @NotNull HandlerList getHandlers() {
+        return handlers;
+    }
+
+    public static HandlerList getHandlerList() {
+        return handlers;
+    }
+}

+ 9 - 3
src/main/java/com/gmail/nossr50/listeners/BlockListener.java

@@ -252,12 +252,18 @@ public class BlockListener implements Listener {
     @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
     @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
     public void onBlockGrow(BlockGrowEvent event)
     public void onBlockGrow(BlockGrowEvent event)
     {
     {
+        Block block = event.getBlock();
+        World world = block.getWorld();
+
         /* WORLD BLACKLIST CHECK */
         /* WORLD BLACKLIST CHECK */
-        if(WorldBlacklist.isWorldBlacklisted(event.getBlock().getWorld()))
+        if(WorldBlacklist.isWorldBlacklisted(world))
             return;
             return;
 
 
-        BlockState blockState = event.getBlock().getState();
-        mcMMO.getPlaceStore().setFalse(blockState);
+        // Minecraft is dumb, the events still throw when a plant "grows" higher than the max block height.  Even though no new block is created
+        if (block.getY() >= world.getMaxHeight())
+            return;
+
+        mcMMO.getPlaceStore().setFalse(block);
     }
     }
 
 
     /**
     /**

+ 5 - 15
src/main/java/com/gmail/nossr50/listeners/ChunkListener.java

@@ -1,30 +1,20 @@
 package com.gmail.nossr50.listeners;
 package com.gmail.nossr50.listeners;
 
 
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType;
-import org.bukkit.entity.Entity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.Listener;
 import org.bukkit.event.world.ChunkUnloadEvent;
 import org.bukkit.event.world.ChunkUnloadEvent;
 
 
+import java.util.List;
+
 public class ChunkListener implements Listener {
 public class ChunkListener implements Listener {
 
 
     @EventHandler(ignoreCancelled = true)
     @EventHandler(ignoreCancelled = true)
     public void onChunkUnload(ChunkUnloadEvent event) {
     public void onChunkUnload(ChunkUnloadEvent event) {
-        for(Entity entity : event.getChunk().getEntities()) {
-            if(entity instanceof LivingEntity) {
-                LivingEntity livingEntity = (LivingEntity) entity;
-                if(mcMMO.getCompatibilityManager().getPersistentDataLayer().hasMobFlag(MobMetaFlagType.COTW_SUMMONED_MOB, livingEntity)) {
-
-                    //Remove from existence
-                    if(livingEntity.isValid()) {
-                        mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity);
-                        livingEntity.setHealth(0);
-                        livingEntity.remove();
-                    }
-                }
-            }
+        List<LivingEntity> matchingEntities = mcMMO.getTransientEntityTracker().getAllTransientEntitiesInChunk(event.getChunk());
+        for(LivingEntity livingEntity : matchingEntities) {
+            mcMMO.getTransientEntityTracker().removeSummon(livingEntity, null, false);
         }
         }
     }
     }
 }
 }

+ 8 - 3
src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -689,11 +689,16 @@ public class EntityListener implements Listener {
      */
      */
     @EventHandler(ignoreCancelled = true)
     @EventHandler(ignoreCancelled = true)
     public void onEntityDeath(EntityDeathEvent event) {
     public void onEntityDeath(EntityDeathEvent event) {
+        LivingEntity entity = event.getEntity();
+
+        if(mcMMO.getTransientEntityTracker().isTransientSummon(entity)) {
+            mcMMO.getTransientEntityTracker().removeSummon(entity, null, false);
+        }
+
         /* WORLD BLACKLIST CHECK */
         /* WORLD BLACKLIST CHECK */
-        if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
+        if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) {
             return;
             return;
-
-        LivingEntity entity = event.getEntity();
+        }
 
 
         if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) {
         if (ExperienceConfig.getInstance().isNPCInteractionPrevented() && Misc.isNPCEntityExcludingVillagers(entity)) {
             return;
             return;

+ 27 - 10
src/main/java/com/gmail/nossr50/listeners/PlayerListener.java

@@ -448,6 +448,10 @@ public class PlayerListener implements Listener {
         if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
         if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld()))
             return;
             return;
 
 
+        if(Misc.isNPCEntityExcludingVillagers(event.getEntity())) {
+            return;
+        }
+
         if(event.getEntity() instanceof Player)
         if(event.getEntity() instanceof Player)
         {
         {
             Player player = (Player) event.getEntity();
             Player player = (Player) event.getEntity();
@@ -463,14 +467,13 @@ public class PlayerListener implements Listener {
                 return;
                 return;
             }
             }
 
 
+            OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player);
+
             //Profile not loaded
             //Profile not loaded
-            if(mcMMO.getUserManager().queryPlayer(player) == null)
-            {
+            if(mmoPlayer == null) {
                 return;
                 return;
             }
             }
 
 
-            OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player);
-
             Item drop = event.getItem();
             Item drop = event.getItem();
             ItemStack dropStack = drop.getItemStack();
             ItemStack dropStack = drop.getItemStack();
 
 
@@ -525,18 +528,32 @@ public class PlayerListener implements Listener {
             return;
             return;
         }
         }
 
 
+        McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player);
+
         //Profile not loaded
         //Profile not loaded
-        OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player);
+        McMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player);
 
 
         if(mmoPlayer == null) {
         if(mmoPlayer == null) {
             return;
             return;
         }
         }
 
 
-        //TODO: There's an issue with using Async saves on player quit
-        //TODO: Basically there are conditions in which an async task does not execute fast enough to save the data if the server shutdown shortly after this task was scheduled
-        mcMMO.getUserManager().saveUserWithDelay(mmoPlayer.getMMOPlayerData(), false, 20);
-
-        mcMMO.getUserManager().cleanupPlayer(mmoPlayer); //Handles cleaning up the player when their profile is no longer needed
+        mcMMO.getUserManager().saveUserImmediately(mmoPlayer, mcMMO.isServerShutdownExecuted());
+        //Use a sync save if the server is shutting down to avoid race conditions
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        //TODO: Make sure this cleans up
+        mmoPlayer.logout(mcMMO.isServerShutdownExecuted());
     }
     }
 
 
     /**
     /**

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

@@ -91,6 +91,8 @@ public class mcMMO extends JavaPlugin {
     private static ChatManager chatManager;
     private static ChatManager chatManager;
     private static CommandManager commandManager; //ACF
     private static CommandManager commandManager; //ACF
     private static SkillRegister skillRegister;
     private static SkillRegister skillRegister;
+    private static TransientEntityTracker transientEntityTracker;
+    private static boolean serverShutdownExecuted = false;
 
 
     /* Adventure */
     /* Adventure */
     private static BukkitAudiences audiences;
     private static BukkitAudiences audiences;
@@ -299,6 +301,9 @@ public class mcMMO extends JavaPlugin {
         chatManager = new ChatManager(this);
         chatManager = new ChatManager(this);
 
 
         commandManager = new CommandManager(this);
         commandManager = new CommandManager(this);
+
+        transientEntityTracker = new TransientEntityTracker();
+        setServerShutdown(false); //Reset flag, used to make decisions about async saves
     }
     }
 
 
     public static PlayerLevelUtils getPlayerLevelUtils() {
     public static PlayerLevelUtils getPlayerLevelUtils() {
@@ -334,6 +339,10 @@ public class mcMMO extends JavaPlugin {
      */
      */
     @Override
     @Override
     public void onDisable() {
     public void onDisable() {
+        setServerShutdown(true);
+        //TODO: Write code to catch unfinished async save tasks, for now we just hope they finish in time, which they should in most cases
+        mcMMO.p.getLogger().info("Server shutdown has been executed, saving and cleaning up data...");
+
         try {
         try {
             userManager.saveAllSync();      // Make sure to save player information if the server shuts down
             userManager.saveAllSync();      // Make sure to save player information if the server shuts down
             userManager.clearAll();
             userManager.clearAll();
@@ -346,17 +355,11 @@ public class mcMMO extends JavaPlugin {
 
 
             formulaManager.saveFormula();
             formulaManager.saveFormula();
             holidayManager.saveAnniversaryFiles();
             holidayManager.saveAnniversaryFiles();
-            placeStore.cleanUp();       // Cleanup empty metadata stores
             placeStore.closeAll();
             placeStore.closeAll();
         }
         }
 
 
         catch (Exception e) { e.printStackTrace(); }
         catch (Exception e) { e.printStackTrace(); }
 
 
-        debug("Canceling all tasks...");
-        getServer().getScheduler().cancelTasks(this); // This removes our tasks
-        debug("Unregister all events...");
-        HandlerList.unregisterAll(this); // Cancel event registrations
-
         if (Config.getInstance().getBackupsEnabled()) {
         if (Config.getInstance().getBackupsEnabled()) {
             // Remove other tasks BEFORE starting the Backup, or we just cancel it straight away.
             // Remove other tasks BEFORE starting the Backup, or we just cancel it straight away.
             try {
             try {
@@ -376,6 +379,11 @@ public class mcMMO extends JavaPlugin {
             }
             }
         }
         }
 
 
+        debug("Canceling all tasks...");
+        getServer().getScheduler().cancelTasks(this); // This removes our tasks
+        debug("Unregister all events...");
+        HandlerList.unregisterAll(this); // Cancel event registrations
+
         databaseManager.onDisable();
         databaseManager.onDisable();
         debug("Was disabled."); // How informative!
         debug("Was disabled."); // How informative!
     }
     }
@@ -704,5 +712,18 @@ public class mcMMO extends JavaPlugin {
         return commandManager;
         return commandManager;
     }
     }
 
 
+    public static TransientEntityTracker getTransientEntityTracker() {
+        return transientEntityTracker;
+    }
+
+    public static synchronized boolean isServerShutdownExecuted() {
+        return serverShutdownExecuted;
+    }
+
+    private static synchronized void setServerShutdown(boolean bool) {
+        serverShutdownExecuted = bool;
+    }
+
+
     public @NotNull SkillRegister getSkillRegister() { return skillRegister; }
     public @NotNull SkillRegister getSkillRegister() { return skillRegister; }
 }
 }

+ 3 - 25
src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java

@@ -37,7 +37,6 @@ import org.bukkit.entity.*;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.PlayerInventory;
 import org.bukkit.inventory.PlayerInventory;
-import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.inventory.meta.SkullMeta;
 import org.bukkit.inventory.meta.SkullMeta;
 import org.bukkit.util.BoundingBox;
 import org.bukkit.util.BoundingBox;
 import org.bukkit.util.Vector;
 import org.bukkit.util.Vector;
@@ -396,7 +395,7 @@ public class FishingManager extends SkillManager {
 
 
         if (treasure != null) {
         if (treasure != null) {
             if(treasure instanceof FishingTreasureBook) {
             if(treasure instanceof FishingTreasureBook) {
-                treasureDrop = createEnchantBook((FishingTreasureBook) treasure);
+                treasureDrop = ItemUtils.createEnchantBook((FishingTreasureBook) treasure);
             } else {
             } else {
                 treasureDrop = treasure.getDrop().clone(); // Not cloning is bad, m'kay?
                 treasureDrop = treasure.getDrop().clone(); // Not cloning is bad, m'kay?
 
 
@@ -450,37 +449,16 @@ public class FishingManager extends SkillManager {
         }
         }
 
 
         if(fishingSucceeds) {
         if(fishingSucceeds) {
-            fishingCatch.setItemStack(treasureDrop);
-
             if (Config.getInstance().getFishingExtraFish()) {
             if (Config.getInstance().getFishingExtraFish()) {
                 Misc.spawnItem(player.getEyeLocation(), fishingCatch.getItemStack(), ItemSpawnReason.FISHING_EXTRA_FISH);
                 Misc.spawnItem(player.getEyeLocation(), fishingCatch.getItemStack(), ItemSpawnReason.FISHING_EXTRA_FISH);
             }
             }
+
+            fishingCatch.setItemStack(treasureDrop);
         }
         }
 
 
         applyXpGain(fishXp + treasureXp, XPGainReason.PVE);
         applyXpGain(fishXp + treasureXp, XPGainReason.PVE);
     }
     }
 
 
-
-    private @NotNull ItemStack createEnchantBook(@NotNull FishingTreasureBook fishingTreasureBook) {
-        ItemStack itemStack = fishingTreasureBook.getDrop().clone();
-        EnchantmentWrapper enchantmentWrapper = getRandomEnchantment(fishingTreasureBook.getLegalEnchantments());
-        ItemMeta itemMeta = itemStack.getItemMeta();
-
-        if(itemMeta == null)
-            return itemStack;
-
-        itemMeta.addEnchant(enchantmentWrapper.getEnchantment(), enchantmentWrapper.getEnchantmentLevel(), ExperienceConfig.getInstance().allowUnsafeEnchantments());
-        itemStack.setItemMeta(itemMeta);
-        return itemStack;
-    }
-
-    private @NotNull EnchantmentWrapper getRandomEnchantment(@NotNull List<EnchantmentWrapper> enchantmentWrappers) {
-        Collections.shuffle(enchantmentWrappers, Misc.getRandom());
-
-        int randomIndex = Misc.getRandom().nextInt(enchantmentWrappers.size());
-        return enchantmentWrappers.get(randomIndex);
-    }
-
     /**
     /**
      * Handle the vanilla XP boost for Fishing
      * Handle the vanilla XP boost for Fishing
      *
      *

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

@@ -32,9 +32,7 @@ import org.bukkit.entity.*;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 
 
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashMap;
-import java.util.List;
 
 
 public class TamingManager extends SkillManager {
 public class TamingManager extends SkillManager {
     //TODO: Temporary static cache, will be changed in 2.2
     //TODO: Temporary static cache, will be changed in 2.2
@@ -42,8 +40,6 @@ public class TamingManager extends SkillManager {
     private static HashMap<CallOfTheWildType, TamingSummon> cotwSummonDataProperties;
     private static HashMap<CallOfTheWildType, TamingSummon> cotwSummonDataProperties;
     private long lastSummonTimeStamp;
     private long lastSummonTimeStamp;
 
 
-    private HashMap<CallOfTheWildType, List<TrackedTamingEntity>> playerSummonedEntities;
-
     public TamingManager(@NotNull OnlineMMOPlayer mmoPlayer) {
     public TamingManager(@NotNull OnlineMMOPlayer mmoPlayer) {
         super(mmoPlayer, PrimarySkillType.TAMING);
         super(mmoPlayer, PrimarySkillType.TAMING);
         init();
         init();
@@ -55,20 +51,12 @@ public class TamingManager extends SkillManager {
         lastSummonTimeStamp = 0L;
         lastSummonTimeStamp = 0L;
 
 
         //Init per-player tracking of summoned entities
         //Init per-player tracking of summoned entities
-        initPerPlayerSummonTracking();
+        mcMMO.getTransientEntityTracker().initPlayer(mmoPlayer.getPlayer());
 
 
         //Hacky stuff used as a band-aid
         //Hacky stuff used as a band-aid
         initStaticCaches();
         initStaticCaches();
     }
     }
 
 
-    private void initPerPlayerSummonTracking() {
-        playerSummonedEntities = new HashMap<>();
-
-        for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
-            playerSummonedEntities.put(callOfTheWildType, new ArrayList<>());
-        }
-    }
-
     private void initStaticCaches() {
     private void initStaticCaches() {
         //TODO: Temporary static cache, will be changed in 2.2
         //TODO: Temporary static cache, will be changed in 2.2
         //This is shared between instances of TamingManager
         //This is shared between instances of TamingManager
@@ -503,58 +491,12 @@ public class TamingManager extends SkillManager {
         return summoningItems.containsKey(itemStack.getType());
         return summoningItems.containsKey(itemStack.getType());
     }
     }
 
 
-    //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
-    private int getAmountCurrentlySummoned(CallOfTheWildType callOfTheWildType) {
-        //The tracker is unreliable so validate its contents first
-        recalibrateTracker();
-
-        return playerSummonedEntities.get(callOfTheWildType).size();
+    private int getAmountCurrentlySummoned(@NotNull CallOfTheWildType callOfTheWildType) {
+        return mcMMO.getTransientEntityTracker().getAmountCurrentlySummoned(getPlayer().getUniqueId(), callOfTheWildType);
     }
     }
 
 
-    //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
     private void addToTracker(@NotNull LivingEntity livingEntity, @NotNull CallOfTheWildType callOfTheWildType) {
     private void addToTracker(@NotNull LivingEntity livingEntity, @NotNull CallOfTheWildType callOfTheWildType) {
-        TrackedTamingEntity trackedEntity = new TrackedTamingEntity(livingEntity, callOfTheWildType, this);
-
-        playerSummonedEntities.get(callOfTheWildType).add(trackedEntity);
-    }
-
-    //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
-    public List<TrackedTamingEntity> getTrackedEntities(CallOfTheWildType callOfTheWildType) {
-        return playerSummonedEntities.get(callOfTheWildType);
-    }
-
-    //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
-    public void removeFromTracker(@NotNull TrackedTamingEntity trackedEntity) {
-        playerSummonedEntities.get(trackedEntity.getCallOfTheWildType()).remove(trackedEntity);
-
-        NotificationManager.sendPlayerInformationChatOnly(getPlayer(), "Taming.Summon.COTW.TimeExpired", StringUtils.getPrettyEntityTypeString(trackedEntity.getLivingEntity().getType()));
-    }
-
-    /**
-     * Builds a new tracked list by determining which tracked things are still valid
-     */
-    //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
-    private void recalibrateTracker() {
-        for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
-            ArrayList<TrackedTamingEntity> validEntities = getValidTrackedEntities(callOfTheWildType);
-            playerSummonedEntities.put(callOfTheWildType, validEntities); //Replace the old list with the new list
-        }
-    }
-
-    //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
-    private @NotNull ArrayList<TrackedTamingEntity> getValidTrackedEntities(@NotNull CallOfTheWildType callOfTheWildType) {
-        ArrayList<TrackedTamingEntity> validTrackedEntities = new ArrayList<>();
-
-        for(TrackedTamingEntity trackedTamingEntity : getTrackedEntities(callOfTheWildType)) {
-            LivingEntity livingEntity = trackedTamingEntity.getLivingEntity();
-
-            //Remove from existence
-            if(livingEntity != null && livingEntity.isValid()) {
-                validTrackedEntities.add(trackedTamingEntity);
-            }
-        }
-
-        return validTrackedEntities;
+        mcMMO.getTransientEntityTracker().registerEntity(getPlayer().getUniqueId(), new TrackedTamingEntity(livingEntity, callOfTheWildType, getPlayer()));
     }
     }
 
 
     /**
     /**
@@ -563,20 +505,6 @@ public class TamingManager extends SkillManager {
      */
      */
     //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
     //TODO: The way this tracker was written is garbo, I should just rewrite it, I'll save that for a future update
     public void cleanupAllSummons() {
     public void cleanupAllSummons() {
-        for(List<TrackedTamingEntity> trackedTamingEntities : playerSummonedEntities.values()) {
-            for(TrackedTamingEntity trackedTamingEntity : trackedTamingEntities) {
-                LivingEntity livingEntity = trackedTamingEntity.getLivingEntity();
-
-                //Remove from existence
-                if(livingEntity != null && livingEntity.isValid()) {
-                    mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity);
-                    livingEntity.setHealth(0);
-                    livingEntity.remove();
-                }
-            }
-
-            //Clear the list
-            trackedTamingEntities.clear();
-        }
+        mcMMO.getTransientEntityTracker().cleanupPlayer(getPlayer());
     }
     }
 }
 }

+ 11 - 32
src/main/java/com/gmail/nossr50/skills/taming/TrackedTamingEntity.java

@@ -4,61 +4,40 @@ import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType;
 import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.Misc;
-import com.gmail.nossr50.util.skills.ParticleEffectUtils;
-import org.bukkit.Location;
-import org.bukkit.Sound;
 import org.bukkit.entity.LivingEntity;
 import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
 import org.bukkit.scheduler.BukkitRunnable;
 import org.bukkit.scheduler.BukkitRunnable;
-
-import java.util.UUID;
+import org.jetbrains.annotations.NotNull;
 
 
 public class TrackedTamingEntity extends BukkitRunnable {
 public class TrackedTamingEntity extends BukkitRunnable {
-    private final LivingEntity livingEntity;
-    private final CallOfTheWildType callOfTheWildType;
-    private final UUID id;
-    private int length;
-    private final TamingManager tamingManagerRef;
+    private final @NotNull LivingEntity livingEntity;
+    private final @NotNull CallOfTheWildType callOfTheWildType;
+    private final @NotNull Player player;
 
 
-    protected TrackedTamingEntity(LivingEntity livingEntity, CallOfTheWildType callOfTheWildType, TamingManager tamingManagerRef) {
-        this.tamingManagerRef = tamingManagerRef;
+    protected TrackedTamingEntity(@NotNull LivingEntity livingEntity, @NotNull CallOfTheWildType callOfTheWildType, @NotNull Player player) {
+        this.player = player;
         this.callOfTheWildType = callOfTheWildType;
         this.callOfTheWildType = callOfTheWildType;
         this.livingEntity = livingEntity;
         this.livingEntity = livingEntity;
-        this.id = livingEntity.getUniqueId();
 
 
         int tamingCOTWLength = Config.getInstance().getTamingCOTWLength(callOfTheWildType.getConfigEntityTypeEntry());
         int tamingCOTWLength = Config.getInstance().getTamingCOTWLength(callOfTheWildType.getConfigEntityTypeEntry());
 
 
         if (tamingCOTWLength > 0) {
         if (tamingCOTWLength > 0) {
-            this.length = tamingCOTWLength * Misc.TICK_CONVERSION_FACTOR;
+            int length = tamingCOTWLength * Misc.TICK_CONVERSION_FACTOR;
             this.runTaskLater(mcMMO.p, length);
             this.runTaskLater(mcMMO.p, length);
         }
         }
     }
     }
 
 
     @Override
     @Override
     public void run() {
     public void run() {
-        if (livingEntity.isValid()) {
-            Location location = livingEntity.getLocation();
-            location.getWorld().playSound(location, Sound.BLOCK_FIRE_EXTINGUISH, 0.8F, 0.8F);
-            ParticleEffectUtils.playCallOfTheWildEffect(livingEntity);
-
-            if(tamingManagerRef != null)
-                tamingManagerRef.removeFromTracker(this);
-
-            livingEntity.setHealth(0);
-            livingEntity.remove();
-        }
-
+        mcMMO.getTransientEntityTracker().removeSummon(this.getLivingEntity(), player, true);
         this.cancel();
         this.cancel();
     }
     }
 
 
-    public CallOfTheWildType getCallOfTheWildType() {
+    public @NotNull CallOfTheWildType getCallOfTheWildType() {
         return callOfTheWildType;
         return callOfTheWildType;
     }
     }
 
 
-    public LivingEntity getLivingEntity() {
+    public @NotNull LivingEntity getLivingEntity() {
         return livingEntity;
         return livingEntity;
     }
     }
-
-    public UUID getID() {
-        return id;
-    }
 }
 }

+ 5 - 2
src/main/java/com/gmail/nossr50/util/BlockUtils.java

@@ -182,11 +182,14 @@ public final class BlockUtils {
      * @return true if the block should affected by Super Breaker, false
      * @return true if the block should affected by Super Breaker, false
      * otherwise
      * otherwise
      */
      */
-    public static Boolean affectedBySuperBreaker(BlockState blockState) {
+    public static boolean affectedBySuperBreaker(BlockState blockState) {
+        if(mcMMO.getMaterialMapStore().isIntendedToolPickaxe(blockState.getType()))
+            return true;
+
         if (ExperienceConfig.getInstance().doesBlockGiveSkillXP(PrimarySkillType.MINING, blockState.getBlockData()))
         if (ExperienceConfig.getInstance().doesBlockGiveSkillXP(PrimarySkillType.MINING, blockState.getBlockData()))
             return true;
             return true;
 
 
-        return isOre(blockState) || mcMMO.getModManager().isCustomMiningBlock(blockState);
+        return mcMMO.getModManager().isCustomMiningBlock(blockState);
     }
     }
 
 
     /**
     /**

+ 27 - 0
src/main/java/com/gmail/nossr50/util/ItemUtils.java

@@ -2,7 +2,10 @@ package com.gmail.nossr50.util;
 
 
 import com.gmail.nossr50.config.AdvancedConfig;
 import com.gmail.nossr50.config.AdvancedConfig;
 import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.config.Config;
+import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.config.party.ItemWeightConfig;
 import com.gmail.nossr50.config.party.ItemWeightConfig;
+import com.gmail.nossr50.datatypes.treasure.EnchantmentWrapper;
+import com.gmail.nossr50.datatypes.treasure.FishingTreasureBook;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import org.bukkit.ChatColor;
 import org.bukkit.ChatColor;
@@ -12,9 +15,11 @@ import org.bukkit.entity.Player;
 import org.bukkit.inventory.FurnaceRecipe;
 import org.bukkit.inventory.FurnaceRecipe;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.Recipe;
 import org.bukkit.inventory.Recipe;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
 
 
+import java.util.Collections;
 import java.util.List;
 import java.util.List;
 
 
 import static org.bukkit.Material.AIR;
 import static org.bukkit.Material.AIR;
@@ -581,4 +586,26 @@ public final class ItemUtils {
     public static boolean canBeSuperAbilityDigBoosted(@NotNull ItemStack itemStack) {
     public static boolean canBeSuperAbilityDigBoosted(@NotNull ItemStack itemStack) {
         return isShovel(itemStack) || isPickaxe(itemStack);
         return isShovel(itemStack) || isPickaxe(itemStack);
     }
     }
+
+    public static @NotNull ItemStack createEnchantBook(@NotNull FishingTreasureBook fishingTreasureBook) {
+        ItemStack itemStack = fishingTreasureBook.getDrop().clone();
+        EnchantmentWrapper enchantmentWrapper = getRandomEnchantment(fishingTreasureBook.getLegalEnchantments());
+        ItemMeta itemMeta = itemStack.getItemMeta();
+
+        if(itemMeta == null) {
+            return itemStack;
+        }
+
+        EnchantmentStorageMeta enchantmentStorageMeta = (EnchantmentStorageMeta) itemMeta;
+        enchantmentStorageMeta.addStoredEnchant(enchantmentWrapper.getEnchantment(), enchantmentWrapper.getEnchantmentLevel(), ExperienceConfig.getInstance().allowUnsafeEnchantments());
+        itemStack.setItemMeta(enchantmentStorageMeta);
+        return itemStack;
+    }
+
+    public static @NotNull EnchantmentWrapper getRandomEnchantment(@NotNull List<EnchantmentWrapper> enchantmentWrappers) {
+        Collections.shuffle(enchantmentWrappers, Misc.getRandom());
+
+        int randomIndex = Misc.getRandom().nextInt(enchantmentWrappers.size());
+        return enchantmentWrappers.get(randomIndex);
+    }
 }
 }

+ 199 - 2
src/main/java/com/gmail/nossr50/util/MaterialMapStore.java

@@ -55,6 +55,8 @@ public class MaterialMapStore {
     private final @NotNull HashSet<String> enchantables;
     private final @NotNull HashSet<String> enchantables;
 
 
     private final @NotNull HashSet<String> ores;
     private final @NotNull HashSet<String> ores;
+    private final @NotNull HashSet<String> intendedToolPickAxe;
+    private final @NotNull HashSet<String> intendedToolShovel;
 
 
     private final @NotNull HashMap<String, Integer> tierValue;
     private final @NotNull HashMap<String, Integer> tierValue;
 
 
@@ -100,14 +102,16 @@ public class MaterialMapStore {
         enchantables = new HashSet<>();
         enchantables = new HashSet<>();
 
 
         ores = new HashSet<>();
         ores = new HashSet<>();
+        intendedToolPickAxe = new HashSet<>();
+        intendedToolShovel = new HashSet<>();
 
 
         tierValue = new HashMap<>();
         tierValue = new HashMap<>();
 
 
         fillVanillaMaterialRegisters();
         fillVanillaMaterialRegisters();
     }
     }
 
 
-    private void fillVanillaMaterialRegisters()
-    {
+    private void fillVanillaMaterialRegisters() {
+        //The order matters
         fillAbilityBlackList();
         fillAbilityBlackList();
         fillToolBlackList();
         fillToolBlackList();
         fillMossyWhiteList();
         fillMossyWhiteList();
@@ -122,6 +126,7 @@ public class MaterialMapStore {
         fillTools();
         fillTools();
         fillEnchantables();
         fillEnchantables();
         fillOres();
         fillOres();
+        fillIntendedTools();
 
 
         fillTierMap();
         fillTierMap();
     }
     }
@@ -206,6 +211,190 @@ public class MaterialMapStore {
         ores.add("gilded_blackstone");
         ores.add("gilded_blackstone");
     }
     }
 
 
+    private void fillIntendedTools() {
+        intendedToolPickAxe.addAll(ores);
+
+        intendedToolPickAxe.add("ice");
+        intendedToolPickAxe.add("packed_ice");
+        intendedToolPickAxe.add("blue_ice");
+        intendedToolPickAxe.add("frosted_ice");
+        intendedToolPickAxe.add("anvil");
+        intendedToolPickAxe.add("bell");
+        intendedToolPickAxe.add("block_of_redstone");
+        intendedToolPickAxe.add("brewing_stand");
+        intendedToolPickAxe.add("cauldron");
+        intendedToolPickAxe.add("chain");
+        intendedToolPickAxe.add("hopper");
+        intendedToolPickAxe.add("iron_bars");
+        intendedToolPickAxe.add("iron_door");
+        intendedToolPickAxe.add("iron_trapdoor");
+        intendedToolPickAxe.add("lantern");
+        intendedToolPickAxe.add("weighted_pressure_plates");
+        intendedToolPickAxe.add("block_of_iron");
+        intendedToolPickAxe.add("copper_blocks");
+        intendedToolPickAxe.add("cut_copper");
+        intendedToolPickAxe.add("cut_copper_slab");
+        intendedToolPickAxe.add("cut_copper_stairs");
+        intendedToolPickAxe.add("lapis_lazuli_block");
+        intendedToolPickAxe.add("lightning_rod");
+        intendedToolPickAxe.add("block_of_diamond");
+        intendedToolPickAxe.add("block_of_emerald");
+        intendedToolPickAxe.add("block_of_gold");
+        intendedToolPickAxe.add("block_of_netherite");
+        intendedToolPickAxe.add("piston");
+        intendedToolPickAxe.add("sticky_piston");
+        intendedToolPickAxe.add("conduit");
+        intendedToolPickAxe.add("shulker_box");
+        intendedToolPickAxe.add("element_constructor"); //be & ee
+        intendedToolPickAxe.add("compound_creator"); //be & ee
+        intendedToolPickAxe.add("material_reducer"); //be & ee
+        intendedToolPickAxe.add("activator_rail");
+        intendedToolPickAxe.add("detector_rail");
+        intendedToolPickAxe.add("powered_rail");
+        intendedToolPickAxe.add("rail");
+        intendedToolPickAxe.add("andesite");
+        intendedToolPickAxe.add("basalt");
+        intendedToolPickAxe.add("blackstone");
+        intendedToolPickAxe.add("blast_furnace");
+        intendedToolPickAxe.add("block_of_coal");
+        intendedToolPickAxe.add("block_of_quartz");
+        intendedToolPickAxe.add("bricks");
+        intendedToolPickAxe.add("cobblestone");
+        intendedToolPickAxe.add("cobblestone_wall");
+        intendedToolPickAxe.add("concrete");
+        intendedToolPickAxe.add("dark_prismarine");
+        intendedToolPickAxe.add("diorite");
+        intendedToolPickAxe.add("dispenser");
+        intendedToolPickAxe.add("dripstone_block");
+        intendedToolPickAxe.add("dropper");
+        intendedToolPickAxe.add("enchantment_table");
+        intendedToolPickAxe.add("end_stone");
+        intendedToolPickAxe.add("ender_chest");
+        intendedToolPickAxe.add("furnace");
+        intendedToolPickAxe.add("glazed_terracotta");
+        intendedToolPickAxe.add("granite");
+        intendedToolPickAxe.add("grindstone");
+        intendedToolPickAxe.add("heat_block"); //be & ee
+        intendedToolPickAxe.add("lodestone");
+        intendedToolPickAxe.add("mossy_cobblestone");
+        intendedToolPickAxe.add("nether_bricks");
+        intendedToolPickAxe.add("nether_brick_fence");
+        intendedToolPickAxe.add("nether_gold_ore");
+        intendedToolPickAxe.add("nether_quartz_ore");
+        intendedToolPickAxe.add("netherrack");
+        intendedToolPickAxe.add("observer");
+        intendedToolPickAxe.add("prismarine");
+        intendedToolPickAxe.add("prismarine_bricks");
+        intendedToolPickAxe.add("pointed_dripstone");
+        intendedToolPickAxe.add("polished_andesite");
+        intendedToolPickAxe.add("polished_blackstone");
+        intendedToolPickAxe.add("polished_blackstone_bricks");
+        intendedToolPickAxe.add("polished_diorite");
+        intendedToolPickAxe.add("polished_granite");
+        intendedToolPickAxe.add("red_sandstone");
+        intendedToolPickAxe.add("sandstone");
+        intendedToolPickAxe.add("smoker");
+        intendedToolPickAxe.add("spawner");
+        intendedToolPickAxe.add("stonecutter");
+//        intendedToolPickAxe.add("slabs");
+        intendedToolPickAxe.add("colored_terracotta");
+//        intendedToolPickAxe.add("stairs");
+        intendedToolPickAxe.add("smooth_stone");
+        intendedToolPickAxe.add("stone");
+        intendedToolPickAxe.add("stone_bricks");
+        intendedToolPickAxe.add("stone_button");
+        intendedToolPickAxe.add("stone_pressure_plate");
+        intendedToolPickAxe.add("terracotta");
+        intendedToolPickAxe.add("amethyst_bud");
+        intendedToolPickAxe.add("amethyst_cluster");
+        intendedToolPickAxe.add("block_of_amethyst");
+        intendedToolPickAxe.add("budding_amethyst");
+        intendedToolPickAxe.add("ancient_debris");
+        intendedToolPickAxe.add("crying_obsidian");
+        intendedToolPickAxe.add("glowing_obsidian"); //be
+        intendedToolPickAxe.add("obsidian");
+        intendedToolPickAxe.add("respawn_anchor");
+
+        //slabs
+        intendedToolPickAxe.add("petrified_oak_slab");
+        intendedToolPickAxe.add("stone_slab");
+        intendedToolPickAxe.add("smooth_stone_slab");
+        intendedToolPickAxe.add("cobblestone_slab");
+        intendedToolPickAxe.add("mossy_cobblestone_slab");
+        intendedToolPickAxe.add("stone_brick_slab");
+        intendedToolPickAxe.add("mossy_stone_brick_slab");
+        intendedToolPickAxe.add("andesite_slab");
+        intendedToolPickAxe.add("polished_andesite_slab");
+        intendedToolPickAxe.add("diorite_slab");
+        intendedToolPickAxe.add("polished_diorite_slab");
+        intendedToolPickAxe.add("granite_slab");
+        intendedToolPickAxe.add("polished_granite_slab");
+        intendedToolPickAxe.add("sandstone_slab");
+        intendedToolPickAxe.add("cut_sandstone_slab");
+        intendedToolPickAxe.add("smooth_sandstone_slab");
+        intendedToolPickAxe.add("red_sandstone_slab");
+        intendedToolPickAxe.add("cut_red_sandstone_slab");
+        intendedToolPickAxe.add("smooth_red_sandstone_slab");
+        intendedToolPickAxe.add("brick_slab");
+        intendedToolPickAxe.add("prismarine_brick_slab");
+        intendedToolPickAxe.add("dark_prismarine_slab");
+        intendedToolPickAxe.add("nether_brick_slab");
+        intendedToolPickAxe.add("red_netherbrick_slab");
+        intendedToolPickAxe.add("quartz_slab");
+        intendedToolPickAxe.add("smooth_quartz_slab");
+        intendedToolPickAxe.add("purpur_slab");
+        intendedToolPickAxe.add("end_stone_brick_slab");
+        intendedToolPickAxe.add("blackstone_slab");
+        intendedToolPickAxe.add("polished_blackstone_slab");
+        intendedToolPickAxe.add("polished_blackstone_brick_slab");
+        intendedToolPickAxe.add("lightly_weathered_cut_copper_slab");
+        intendedToolPickAxe.add("semi_weathered_cut_copper_slab");
+        intendedToolPickAxe.add("waxed_semi_weathered_cut_copper_slab");
+        intendedToolPickAxe.add("weathered_cut_copper_slab");
+        intendedToolPickAxe.add("waxed_cut_copper_slab");
+        intendedToolPickAxe.add("waxed_lightly_weathered_cut_copper_slab");
+
+        //stairs (not all of these exist, just copied the above list and replaced slab with stairs)
+        intendedToolPickAxe.add("petrified_oak_stairs");
+        intendedToolPickAxe.add("stone_stairs");
+        intendedToolPickAxe.add("smooth_stone_stairs");
+        intendedToolPickAxe.add("cobblestone_stairs");
+        intendedToolPickAxe.add("mossy_cobblestone_stairs");
+        intendedToolPickAxe.add("stone_brick_stairs");
+        intendedToolPickAxe.add("mossy_stone_brick_stairs");
+        intendedToolPickAxe.add("andesite_stairs");
+        intendedToolPickAxe.add("polished_andesite_stairs");
+        intendedToolPickAxe.add("diorite_stairs");
+        intendedToolPickAxe.add("polished_diorite_stairs");
+        intendedToolPickAxe.add("granite_stairs");
+        intendedToolPickAxe.add("polished_granite_stairs");
+        intendedToolPickAxe.add("sandstone_stairs");
+        intendedToolPickAxe.add("cut_sandstone_stairs");
+        intendedToolPickAxe.add("smooth_sandstone_stairs");
+        intendedToolPickAxe.add("red_sandstone_stairs");
+        intendedToolPickAxe.add("cut_red_sandstone_stairs");
+        intendedToolPickAxe.add("smooth_red_sandstone_stairs");
+        intendedToolPickAxe.add("brick_stairs");
+        intendedToolPickAxe.add("prismarine_brick_stairs");
+        intendedToolPickAxe.add("dark_prismarine_stairs");
+        intendedToolPickAxe.add("nether_brick_stairs");
+        intendedToolPickAxe.add("red_netherbrick_stairs");
+        intendedToolPickAxe.add("quartz_stairs");
+        intendedToolPickAxe.add("smooth_quartz_stairs");
+        intendedToolPickAxe.add("purpur_stairs");
+        intendedToolPickAxe.add("end_stone_brick_stairs");
+        intendedToolPickAxe.add("blackstone_stairs");
+        intendedToolPickAxe.add("polished_blackstone_stairs");
+        intendedToolPickAxe.add("polished_blackstone_brick_stairs");
+        intendedToolPickAxe.add("lightly_weathered_cut_copper_stairs");
+        intendedToolPickAxe.add("semi_weathered_cut_copper_stairs");
+        intendedToolPickAxe.add("waxed_semi_weathered_cut_copper_stairs");
+        intendedToolPickAxe.add("weathered_cut_copper_stairs");
+        intendedToolPickAxe.add("waxed_cut_copper_stairs");
+        intendedToolPickAxe.add("waxed_lightly_weathered_cut_copper_stairs");
+
+    }
+
     private void fillArmors() {
     private void fillArmors() {
         fillLeatherArmorWhiteList();
         fillLeatherArmorWhiteList();
         fillIronArmorWhiteList();
         fillIronArmorWhiteList();
@@ -1078,6 +1267,14 @@ public class MaterialMapStore {
         toolBlackList.add("respawn_anchor");
         toolBlackList.add("respawn_anchor");
     }
     }
 
 
+    public boolean isIntendedToolPickaxe(Material material) {
+        return intendedToolPickAxe.contains(material.getKey().getKey());
+    }
+
+    public boolean isIntendedToolPickaxe(String string) {
+        return intendedToolPickAxe.contains(string);
+    }
+
     public @NotNull HashSet<String> getNetheriteArmor() {
     public @NotNull HashSet<String> getNetheriteArmor() {
         return netheriteArmor;
         return netheriteArmor;
     }
     }

+ 316 - 0
src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java

@@ -0,0 +1,316 @@
+package com.gmail.nossr50.util;
+
+import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.skills.taming.TrackedTamingEntity;
+import com.gmail.nossr50.util.player.NotificationManager;
+import com.gmail.nossr50.util.skills.ParticleEffectUtils;
+import com.gmail.nossr50.util.text.StringUtils;
+import com.google.common.collect.ImmutableSet;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.Sound;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+public class TransientEntityTracker {
+    //These two are updated in step with each other
+    private final @NotNull HashMap<UUID, HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>> perPlayerTransientEntityMap;
+    private final @NotNull HashSet<LivingEntity> chunkLookupCache;
+
+    public TransientEntityTracker() {
+        perPlayerTransientEntityMap = new HashMap<>();
+        chunkLookupCache = new HashSet<>();
+    }
+
+    public synchronized @NotNull HashSet<LivingEntity> getChunkLookupCache() {
+        return chunkLookupCache;
+    }
+
+    public synchronized @NotNull HashMap<UUID, HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>> getPerPlayerTransientEntityMap() {
+        return perPlayerTransientEntityMap;
+    }
+
+    public synchronized void initPlayer(@NotNull Player player) {
+        if (!isPlayerRegistered(player.getUniqueId())) {
+            registerPlayer(player.getUniqueId());
+        }
+    }
+
+    /**
+     * Removes a player from the tracker
+     *
+     * @param playerUUID target player
+     */
+    public synchronized void cleanupPlayer(@NotNull UUID playerUUID) {
+        cleanPlayer(null, playerUUID);
+    }
+
+    /**
+     * Removes a player from the tracker
+     *
+     * @param player target player
+     */
+    public synchronized void cleanupPlayer(@NotNull Player player) {
+        cleanPlayer(player, player.getUniqueId());
+    }
+
+    /**
+     * Removes a player from the tracker
+     *
+     * @param player target player
+     * @param playerUUID target player UUID
+     */
+    private void cleanPlayer(@Nullable Player player, @NotNull UUID playerUUID) {
+        cleanupAllSummons(player, player.getUniqueId());
+        removePlayerFromMap(playerUUID);
+    }
+
+    private void removePlayerFromMap(@NotNull UUID playerUUID) {
+        getPerPlayerTransientEntityMap().remove(playerUUID);
+    }
+
+    /**
+     * Checks if a player has already been registered
+     * Being registered constitutes having necessary values initialized in our per-player map
+     *
+     * @param playerUUID target player
+     * @return true if the player is registered
+     */
+    private synchronized boolean isPlayerRegistered(@NotNull UUID playerUUID) {
+        return getPerPlayerTransientEntityMap().get(playerUUID) != null;
+    }
+
+    /**
+     * Register a player to our tracker, which initializes the necessary values in our per-player map
+     *
+     * @param playerUUID player to register
+     */
+    private synchronized void registerPlayer(@NotNull UUID playerUUID) {
+        getPerPlayerTransientEntityMap().put(playerUUID, new HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>>());
+
+        for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
+            getPerPlayerTransientEntityMap().get(playerUUID).put(callOfTheWildType, new HashSet<>());
+        }
+    }
+
+    /**
+     * Get the tracked transient entities map for a specific player
+     *
+     * @param playerUUID the target uuid of the player
+     * @return the tracked entities map for the player, null if the player isn't registered
+     */
+    public synchronized @Nullable HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>> getPlayerTrackedEntityMap(@NotNull UUID playerUUID) {
+        return getPerPlayerTransientEntityMap().get(playerUUID);
+    }
+
+    /**
+     * Registers an entity to a player
+     * This includes registration to our per-player map and our chunk lookup cache
+     *
+     * @param playerUUID target player's UUID
+     * @param trackedTamingEntity target entity
+     */
+    public synchronized void registerEntity(@NotNull UUID playerUUID, @NotNull TrackedTamingEntity trackedTamingEntity) {
+        //Add to map entry
+        getTrackedEntities(playerUUID, trackedTamingEntity.getCallOfTheWildType()).add(trackedTamingEntity);
+
+        //Add to cache for chunk lookups
+        addToChunkLookupCache(trackedTamingEntity);
+    }
+
+    /**
+     * Checks if a living entity is a summon
+     *
+     * @param livingEntity target livinig entity
+     * @return true if target living entity is a summon
+     */
+    public synchronized boolean isTransientSummon(@NotNull LivingEntity livingEntity) {
+        return getChunkLookupCache().contains(livingEntity);
+    }
+
+    /**
+     * Get the tracked taming entities for a player
+     * If the player isn't registered this will return null
+     *
+     * @param playerUUID the target uuid of the player
+     * @param callOfTheWildType target type
+     * @return the set of tracked entities for the player, null if the player isn't registered, the set can be empty
+     */
+    private synchronized @Nullable HashSet<TrackedTamingEntity> getTrackedEntities(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) {
+        HashMap<CallOfTheWildType, HashSet<TrackedTamingEntity>> playerEntityMap = getPlayerTrackedEntityMap(playerUUID);
+
+        if(playerEntityMap == null)
+            return null;
+
+        return playerEntityMap.get(callOfTheWildType);
+    }
+
+    /**
+     * Adds an entity to our chunk lookup cache
+     *
+     * @param trackedTamingEntity target tracked taming entity
+     */
+    private synchronized void addToChunkLookupCache(@NotNull TrackedTamingEntity trackedTamingEntity) {
+        getChunkLookupCache().add(trackedTamingEntity.getLivingEntity());
+    }
+
+    /**
+     * Removes an entity from our tracker
+     * This includes removal from our per-player map and our chunk lookup cache
+     *
+     * @param livingEntity target entity
+     */
+    private void unregisterEntity(@NotNull LivingEntity livingEntity) {
+        chunkLookupCacheCleanup(livingEntity);
+        perPlayerTransientMapCleanup(livingEntity);
+    }
+
+    /**
+     * Removes an entity from our chunk lookup cache
+     *
+     * @param livingEntity target entity
+     */
+    private void chunkLookupCacheCleanup(@NotNull LivingEntity livingEntity) {
+        getChunkLookupCache().remove(livingEntity);
+    }
+
+    /**
+     * Clean a living entity from our tracker
+     * Iterates over all players and their registered entities
+     * Doesn't do any kind of failure checking, if it doesn't find any player with a registered entity nothing bad happens or is reported
+     * However it should never happen like that, so maybe we could consider adding some failure to execute checking in the future
+     *
+     * @param livingEntity
+     */
+    private void perPlayerTransientMapCleanup(@NotNull LivingEntity livingEntity) {
+        for(UUID uuid : getPerPlayerTransientEntityMap().keySet()) {
+            for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
+
+                HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(uuid, callOfTheWildType);
+
+                if(trackedEntities == null)
+                    continue;
+
+                Iterator<TrackedTamingEntity> iterator = trackedEntities.iterator();
+                while (iterator.hasNext()) {
+                    if(iterator.next().getLivingEntity().equals(livingEntity)) {
+                        iterator.remove();
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Get all transient entities that exist in a specific chunk
+     *
+     * @param chunk the chunk to match
+     * @return a list of transient entities that are located in the provided chunk
+     */
+    public synchronized @NotNull List<LivingEntity> getAllTransientEntitiesInChunk(@NotNull Chunk chunk) {
+        ArrayList<LivingEntity> matchingEntities = new ArrayList<>();
+
+        for(LivingEntity livingEntity : getChunkLookupCache()) {
+            if(livingEntity.getLocation().getChunk().equals(chunk)) {
+                matchingEntities.add(livingEntity);
+            }
+        }
+
+        return matchingEntities;
+    }
+
+    /**
+     * Get the amount of a summon currently active for a player
+     *
+     * @param playerUUID target player
+     * @param callOfTheWildType summon type
+     * @return the amount of summons currently active for player of target type
+     */
+    public synchronized int getAmountCurrentlySummoned(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) {
+        HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(playerUUID, callOfTheWildType);
+
+        if(trackedEntities == null)
+            return 0;
+
+        return trackedEntities.size();
+    }
+
+    /**
+     * Kills a summon and removes its metadata
+     * Then it removes it from the tracker / chunk lookup cache
+     *
+     * @param livingEntity entity to remove
+     * @param player associated player
+     */
+    public synchronized void removeSummon(@NotNull LivingEntity livingEntity, @Nullable Player player, boolean timeExpired) {
+        //Kill the summon & remove it
+        if(livingEntity.isValid()) {
+            livingEntity.setHealth(0); //Should trigger entity death events
+            livingEntity.remove();
+
+            Location location = livingEntity.getLocation();
+
+            if (location.getWorld() != null) {
+                location.getWorld().playSound(location, Sound.BLOCK_FIRE_EXTINGUISH, 0.8F, 0.8F);
+                ParticleEffectUtils.playCallOfTheWildEffect(livingEntity);
+            }
+
+            //Inform player of summon death
+            if(player != null && player.isOnline()) {
+                if(timeExpired) {
+                    NotificationManager.sendPlayerInformationChatOnly(player, "Taming.Summon.COTW.TimeExpired", StringUtils.getPrettyEntityTypeString(livingEntity.getType()));
+                } else {
+                    NotificationManager.sendPlayerInformationChatOnly(player, "Taming.Summon.COTW.Removed", StringUtils.getPrettyEntityTypeString(livingEntity.getType()));
+                }
+            }
+        }
+
+        //Remove our metadata
+        mcMMO.getCompatibilityManager().getPersistentDataLayer().removeMobFlags(livingEntity);
+
+        //Clean from trackers
+        unregisterEntity(livingEntity);
+    }
+
+    /**
+     * Remove all tracked entities from existence if they currently exist
+     * Clear the tracked entity lists afterwards
+     *
+     * @deprecated use {@link #cleanupAllSummons(Player, UUID)} instead
+     */
+    @Deprecated
+    private void cleanupAllSummons(@NotNull UUID playerUUID) {
+        cleanupAllSummons(Bukkit.getPlayer(playerUUID), playerUUID);
+    }
+
+    /**
+     * Kills and cleans up all data related to all summoned entities for a player
+     *
+     * @param player used to send messages, can be null
+     * @param playerUUID used to grab associated data, cannot be null
+     */
+    private void cleanupAllSummons(@Nullable Player player, @NotNull UUID playerUUID) {
+        for(CallOfTheWildType callOfTheWildType : CallOfTheWildType.values()) {
+            HashSet<TrackedTamingEntity> trackedEntities = getTrackedEntities(playerUUID, callOfTheWildType);
+
+            if(trackedEntities == null) {
+                continue;
+            }
+
+            ImmutableSet<TrackedTamingEntity> immutableSet = ImmutableSet.copyOf(trackedEntities);
+
+            for(TrackedTamingEntity trackedTamingEntity : immutableSet) {
+                //Remove from existence
+                removeSummon(trackedTamingEntity.getLivingEntity(), player, false);
+            }
+
+        }
+    }
+}

+ 111 - 78
src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java

@@ -2,33 +2,38 @@ package com.gmail.nossr50.util.blockmeta;
 
 
 import org.bukkit.Bukkit;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
 import org.bukkit.World;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 
 import java.io.*;
 import java.io.*;
 import java.util.BitSet;
 import java.util.BitSet;
 import java.util.UUID;
 import java.util.UUID;
 
 
-public class BitSetChunkStore implements ChunkStore, Serializable {
-    private static final long serialVersionUID = -1L;
-    transient private boolean dirty = false;
-    // Bitset store conforms to a "bottom-up" bit ordering consisting of a stack of {worldHeight} Y planes, each Y plane consists of 16 Z rows of 16 X bits.
-    private BitSet store;
+public class BitSetChunkStore implements ChunkStore {
     private static final int CURRENT_VERSION = 8;
     private static final int CURRENT_VERSION = 8;
     private static final int MAGIC_NUMBER = 0xEA5EDEBB;
     private static final int MAGIC_NUMBER = 0xEA5EDEBB;
-    private int cx;
-    private int cz;
-    private int worldHeight;
-    private UUID worldUid;
 
 
-    public BitSetChunkStore(World world, int cx, int cz) {
+    private final int cx;
+    private final int cz;
+    private final int worldHeight;
+    private final @NotNull UUID worldUid;
+    // Bitset store conforms to a "bottom-up" bit ordering consisting of a stack of {worldHeight} Y planes, each Y plane consists of 16 Z rows of 16 X bits.
+    private final @NotNull BitSet store;
+
+    private transient boolean dirty = false;
+
+    public BitSetChunkStore(@NotNull World world, int cx, int cz) {
+        this(world.getUID(), world.getMaxHeight(), cx, cz);
+    }
+
+    private BitSetChunkStore(@NotNull UUID worldUid, int worldHeight, int cx, int cz) {
         this.cx = cx;
         this.cx = cx;
         this.cz = cz;
         this.cz = cz;
-        this.worldUid = world.getUID();
-        this.worldHeight = world.getMaxHeight();
+        this.worldUid = worldUid;
+        this.worldHeight = worldHeight;
         this.store = new BitSet(16 * 16 * worldHeight);
         this.store = new BitSet(16 * 16 * worldHeight);
     }
     }
 
 
-    private BitSetChunkStore() {}
-
     @Override
     @Override
     public boolean isDirty() {
     public boolean isDirty() {
         return dirty;
         return dirty;
@@ -50,7 +55,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
     }
     }
 
 
     @Override
     @Override
-    public UUID getWorldId() {
+    public @NotNull UUID getWorldId() {
         return worldUid;
         return worldUid;
     }
     }
 
 
@@ -81,61 +86,27 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
     }
     }
 
 
     private int coordToIndex(int x, int y, int z) {
     private int coordToIndex(int x, int y, int z) {
+        return coordToIndex(x, y, z, worldHeight);
+    }
+
+    private static int coordToIndex(int x, int y, int z, int worldHeight) {
         if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16)
         if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16)
-            throw new IndexOutOfBoundsException();
+            throw new IndexOutOfBoundsException(String.format("x: %d y: %d z: %d World Height: %d", x, y, z, worldHeight));
         return (z * 16 + x) + (256 * y);
         return (z * 16 + x) + (256 * y);
     }
     }
 
 
-    private void fixWorldHeight() {
+    private static int getWorldHeight(@NotNull UUID worldUid, int storedWorldHeight)
+    {
         World world = Bukkit.getWorld(worldUid);
         World world = Bukkit.getWorld(worldUid);
 
 
         // Not sure how this case could come up, but might as well handle it gracefully.  Loading a chunkstore for an unloaded world?
         // Not sure how this case could come up, but might as well handle it gracefully.  Loading a chunkstore for an unloaded world?
         if (world == null)
         if (world == null)
-            return;
-
-        // Lop off any extra data if the world height has shrunk
-        int currentWorldHeight = world.getMaxHeight();
-        if (currentWorldHeight < worldHeight)
-        {
-            store.clear(coordToIndex(16, currentWorldHeight, 16), store.length());
-            worldHeight = currentWorldHeight;
-            dirty = true;
-        }
-        // If the world height has grown, update the worldHeight variable, but don't bother marking it dirty as unless something else changes we don't need to force a file write;
-        else if (currentWorldHeight > worldHeight)
-            worldHeight = currentWorldHeight;
-    }
-
-    @Deprecated
-    private void writeObject(ObjectOutputStream out) throws IOException {
-        throw new UnsupportedOperationException("Serializable support should only be used for legacy deserialization");
-    }
+            return storedWorldHeight;
 
 
-    @Deprecated
-    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
-        in.readInt(); // Magic number
-        in.readInt(); // Format version
-        long lsb = in.readLong();
-        long msb = in.readLong();
-        worldUid = new UUID(msb, lsb);
-        cx = in.readInt();
-        cz = in.readInt();
-
-        boolean[][][] oldStore = (boolean[][][]) in.readObject();
-        worldHeight = oldStore[0][0].length;
-        store = new BitSet(16 * 16 * worldHeight / 8);
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < worldHeight; y++) {
-                    store.set(coordToIndex(x, y, z), oldStore[x][z][y]);
-                }
-            }
-        }
-        dirty = true;
-        fixWorldHeight();
+        return world.getMaxHeight();
     }
     }
 
 
-    private void serialize(DataOutputStream out) throws IOException {
+    private void serialize(@NotNull DataOutputStream out) throws IOException {
         out.writeInt(MAGIC_NUMBER);
         out.writeInt(MAGIC_NUMBER);
         out.writeInt(CURRENT_VERSION);
         out.writeInt(CURRENT_VERSION);
 
 
@@ -153,7 +124,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
         dirty = false;
         dirty = false;
     }
     }
 
 
-    private static BitSetChunkStore deserialize(DataInputStream in) throws IOException {
+    private static @NotNull BitSetChunkStore deserialize(@NotNull DataInputStream in) throws IOException {
         int magic = in.readInt();
         int magic = in.readInt();
         // Can be used to determine the format of the file
         // Can be used to determine the format of the file
         int fileVersionNumber = in.readInt();
         int fileVersionNumber = in.readInt();
@@ -161,28 +132,36 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
         if (magic != MAGIC_NUMBER || fileVersionNumber != CURRENT_VERSION)
         if (magic != MAGIC_NUMBER || fileVersionNumber != CURRENT_VERSION)
             throw new IOException();
             throw new IOException();
 
 
-        BitSetChunkStore chunkStore = new BitSetChunkStore();
-
         long lsb = in.readLong();
         long lsb = in.readLong();
         long msb = in.readLong();
         long msb = in.readLong();
-        chunkStore.worldUid = new UUID(msb, lsb);
-        chunkStore.cx = in.readInt();
-        chunkStore.cz = in.readInt();
+        UUID worldUid = new UUID(msb, lsb);
+        int cx = in.readInt();
+        int cz = in.readInt();
 
 
-        chunkStore.worldHeight = in.readInt();
+        int worldHeight = in.readInt();
         byte[] temp = new byte[in.readInt()];
         byte[] temp = new byte[in.readInt()];
         in.readFully(temp);
         in.readFully(temp);
-        chunkStore.store = BitSet.valueOf(temp);
+        BitSet stored = BitSet.valueOf(temp);
+
+        int currentWorldHeight = getWorldHeight(worldUid, worldHeight);
+
+        boolean worldHeightShrunk = currentWorldHeight < worldHeight;
+        // Lop off extra data if world height has shrunk
+        if (worldHeightShrunk)
+            stored.clear(coordToIndex(16, currentWorldHeight, 16, worldHeight), stored.length());
+
+        BitSetChunkStore chunkStore = new BitSetChunkStore(worldUid, currentWorldHeight, cx, cz);
+        chunkStore.store.or(stored);
+        chunkStore.dirty = worldHeightShrunk; // In the expanded case there is no reason to re-write it unless the data changes
 
 
-        chunkStore.fixWorldHeight();
         return chunkStore;
         return chunkStore;
     }
     }
 
 
     public static class Serialization {
     public static class Serialization {
 
 
-        public static final short STREAM_MAGIC = (short)0xACDC;
+        public static final short STREAM_MAGIC = (short)0xACDC; // Rock on
 
 
-        public static ChunkStore readChunkStore(DataInputStream inputStream) throws IOException {
+        public static @Nullable ChunkStore readChunkStore(@NotNull DataInputStream inputStream) throws IOException {
             if (inputStream.markSupported())
             if (inputStream.markSupported())
                 inputStream.mark(2);
                 inputStream.mark(2);
             short magicNumber = inputStream.readShort();
             short magicNumber = inputStream.readShort();
@@ -196,7 +175,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
                 {
                 {
                     // Creates a new stream with the two magic number bytes and then the rest of the original stream...   Java is so dumb.  I just wanted to look at two bytes.
                     // Creates a new stream with the two magic number bytes and then the rest of the original stream...   Java is so dumb.  I just wanted to look at two bytes.
                     PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream, 2);
                     PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream, 2);
-                    pushbackInputStream.unread((magicNumber >>> 0) & 0xFF);
+                    pushbackInputStream.unread((magicNumber) & 0xFF);
                     pushbackInputStream.unread((magicNumber >>> 8) & 0xFF);
                     pushbackInputStream.unread((magicNumber >>> 8) & 0xFF);
                     inputStream = new DataInputStream(pushbackInputStream);
                     inputStream = new DataInputStream(pushbackInputStream);
                 }
                 }
@@ -209,31 +188,85 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
             throw new IOException("Bad Data Format");
             throw new IOException("Bad Data Format");
         }
         }
 
 
-        public static void writeChunkStore(DataOutputStream outputStream, ChunkStore chunkStore) throws IOException {
+        public static void writeChunkStore(@NotNull DataOutputStream outputStream, @NotNull ChunkStore chunkStore) throws IOException {
             if (!(chunkStore instanceof BitSetChunkStore))
             if (!(chunkStore instanceof BitSetChunkStore))
                 throw new InvalidClassException("ChunkStore must be instance of BitSetChunkStore");
                 throw new InvalidClassException("ChunkStore must be instance of BitSetChunkStore");
             outputStream.writeShort(STREAM_MAGIC);
             outputStream.writeShort(STREAM_MAGIC);
             ((BitSetChunkStore)chunkStore).serialize(outputStream);
             ((BitSetChunkStore)chunkStore).serialize(outputStream);
         }
         }
 
 
-        // Handles loading the old serialized classes even though we have changed name/package
+        // Handles loading the old serialized class
         private static class LegacyDeserializationInputStream extends ObjectInputStream {
         private static class LegacyDeserializationInputStream extends ObjectInputStream {
-            public LegacyDeserializationInputStream(InputStream in) throws IOException {
+            private static class LegacyChunkStoreDeserializer implements Serializable
+            {
+                private static final long serialVersionUID = -1L;
+
+                private int cx;
+                private int cz;
+                private int worldHeight;
+                private UUID worldUid;
+                private boolean[][][] store;
+
+                private LegacyChunkStoreDeserializer() {}
+
+                @Deprecated
+                private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
+                    throw new UnsupportedOperationException("You goofed.");
+                }
+
+                @Deprecated
+                private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
+                    in.readInt(); // Magic number
+                    in.readInt(); // Format version
+                    long lsb = in.readLong();
+                    long msb = in.readLong();
+
+                    worldUid = new UUID(msb, lsb);
+                    cx = in.readInt();
+                    cz = in.readInt();
+
+                    store = (boolean[][][]) in.readObject();
+                    worldHeight = store[0][0].length;
+                }
+
+                public @NotNull BitSetChunkStore convert()
+                {
+                    int currentWorldHeight = getWorldHeight(worldUid, worldHeight);
+
+                    BitSetChunkStore converted = new BitSetChunkStore(worldUid, currentWorldHeight, cx, cz);
+
+                    // Read old data into new chunkstore
+                    for (int x = 0; x < 16; x++) {
+                        for (int z = 0; z < 16; z++) {
+                            for (int y = 0; y < worldHeight && y < currentWorldHeight; y++) {
+                                converted.store.set(converted.coordToIndex(x, y, z), store[x][z][y]);
+                            }
+                        }
+                    }
+                    // Mark dirty so it will be re-written in new format on close
+                    converted.dirty = true;
+                    return converted;
+                }
+            }
+
+
+            public LegacyDeserializationInputStream(@NotNull InputStream in) throws IOException {
                 super(in);
                 super(in);
                 enableResolveObject(true);
                 enableResolveObject(true);
             }
             }
 
 
             @Override
             @Override
-            protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
+            protected @NotNull ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
                 ObjectStreamClass read = super.readClassDescriptor();
                 ObjectStreamClass read = super.readClassDescriptor();
                 if (read.getName().contentEquals("com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore"))
                 if (read.getName().contentEquals("com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore"))
-                    return ObjectStreamClass.lookup(BitSetChunkStore.class);
+                    return ObjectStreamClass.lookup(LegacyChunkStoreDeserializer.class);
                 return read;
                 return read;
             }
             }
 
 
-            public ChunkStore readLegacyChunkStore(){
+            public @Nullable ChunkStore readLegacyChunkStore(){
                 try {
                 try {
-                    return (ChunkStore) readObject();
+                    LegacyChunkStoreDeserializer deserializer = (LegacyChunkStoreDeserializer)readObject();
+                    return deserializer.convert();
                 } catch (IOException | ClassNotFoundException e) {
                 } catch (IOException | ClassNotFoundException e) {
                     return null;
                     return null;
                 }
                 }

+ 4 - 120
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java

@@ -1,126 +1,10 @@
 package com.gmail.nossr50.util.blockmeta;
 package com.gmail.nossr50.util.blockmeta;
 
 
 import org.bukkit.World;
 import org.bukkit.World;
-import org.bukkit.block.Block;
-import org.bukkit.block.BlockState;
+import org.jetbrains.annotations.NotNull;
 
 
-public interface ChunkManager {
+public interface ChunkManager extends UserBlockTracker {
     void closeAll();
     void closeAll();
-
-    /**
-     * Saves a given Chunk's Chunklet data
-     *
-     * @param cx Chunk X coordinate that is to be saved
-     * @param cz Chunk Z coordinate that is to be saved
-     * @param world World that the Chunk is in
-     */
-    void saveChunk(int cx, int cz, World world);
-
-    /**
-     * Informs the ChunkletManager a chunk is unloaded
-     *
-     * @param cx Chunk X coordinate that is unloaded
-     * @param cz Chunk Z coordinate that is unloaded
-     * @param world World that the chunk was unloaded in
-     */
-    void chunkUnloaded(int cx, int cz, World world);
-
-    /**
-     * Save all ChunkletStores related to the given world
-     *
-     * @param world World to save
-     */
-    void saveWorld(World world);
-
-    /**
-     * Unload all ChunkletStores from memory related to the given world after saving them
-     *
-     * @param world World to unload
-     */
-    void unloadWorld(World world);
-
-    /**
-     * Save all ChunkletStores
-     */
-    void saveAll();
-
-    /**
-     * Check to see if a given location is set to true
-     *
-     * @param x X coordinate to check
-     * @param y Y coordinate to check
-     * @param z Z coordinate to check
-     * @param world World to check in
-     * @return true if the given location is set to true, false if otherwise
-     */
-    boolean isTrue(int x, int y, int z, World world);
-
-    /**
-     * Check to see if a given block location is set to true
-     *
-     * @param block Block location to check
-     * @return true if the given block location is set to true, false if otherwise
-     */
-    boolean isTrue(Block block);
-
-    /**
-     * Check to see if a given BlockState location is set to true
-     *
-     * @param blockState BlockState to check
-     * @return true if the given BlockState location is set to true, false if otherwise
-     */
-    boolean isTrue(BlockState blockState);
-
-    /**
-     * Set a given location to true, should create stores as necessary if the location does not exist
-     *
-     * @param x X coordinate to set
-     * @param y Y coordinate to set
-     * @param z Z coordinate to set
-     * @param world World to set in
-     */
-    void setTrue(int x, int y, int z, World world);
-
-    /**
-     * Set a given block location to true, should create stores as necessary if the location does not exist
-     *
-     * @param block Block location to set
-     */
-    void setTrue(Block block);
-
-    /**
-     * Set a given BlockState location to true, should create stores as necessary if the location does not exist
-     *
-     * @param blockState BlockState location to set
-     */
-    void setTrue(BlockState blockState);
-
-    /**
-     * Set a given location to false, should not create stores if one does not exist for the given location
-     *
-     * @param x X coordinate to set
-     * @param y Y coordinate to set
-     * @param z Z coordinate to set
-     * @param world World to set in
-     */
-    void setFalse(int x, int y, int z, World world);
-
-    /**
-     * Set a given block location to false, should not create stores if one does not exist for the given location
-     *
-     * @param block Block location to set
-     */
-    void setFalse(Block block);
-
-    /**
-     * Set a given BlockState location to false, should not create stores if one does not exist for the given location
-     *
-     * @param blockState BlockState location to set
-     */
-    void setFalse(BlockState blockState);
-
-    /**
-     * Delete any ChunkletStores that are empty
-     */
-    void cleanUp();
+    void chunkUnloaded(int cx, int cz, @NotNull World world);
+    void unloadWorld(@NotNull World world);
 }
 }

+ 2 - 1
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java

@@ -1,9 +1,10 @@
 package com.gmail.nossr50.util.blockmeta;
 package com.gmail.nossr50.util.blockmeta;
 
 
 import com.gmail.nossr50.config.HiddenConfig;
 import com.gmail.nossr50.config.HiddenConfig;
+import org.jetbrains.annotations.NotNull;
 
 
 public class ChunkManagerFactory {
 public class ChunkManagerFactory {
-    public static ChunkManager getChunkManager() {
+    public static @NotNull ChunkManager getChunkManager() {
         HiddenConfig hConfig = HiddenConfig.getInstance();
         HiddenConfig hConfig = HiddenConfig.getInstance();
 
 
         if (hConfig.getChunkletsEnabled()) {
         if (hConfig.getChunkletsEnabled()) {

+ 2 - 2
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java

@@ -1,6 +1,6 @@
 package com.gmail.nossr50.util.blockmeta;
 package com.gmail.nossr50.util.blockmeta;
 
 
-import org.bukkit.World;
+import org.jetbrains.annotations.NotNull;
 
 
 import java.util.UUID;
 import java.util.UUID;
 
 
@@ -36,7 +36,7 @@ public interface ChunkStore {
      */
      */
     int getChunkZ();
     int getChunkZ();
 
 
-    UUID getWorldId();
+    @NotNull UUID getWorldId();
 
 
     /**
     /**
      * Checks the value at the given coordinates
      * Checks the value at the given coordinates

+ 34 - 116
src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java

@@ -1,12 +1,16 @@
 package com.gmail.nossr50.util.blockmeta;
 package com.gmail.nossr50.util.blockmeta;
 
 
-import com.gmail.nossr50.mcMMO;
 import org.bukkit.Bukkit;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
 import org.bukkit.World;
 import org.bukkit.block.Block;
 import org.bukkit.block.Block;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.BlockState;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 
-import java.io.*;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
 import java.util.*;
 import java.util.*;
 
 
 public class HashChunkManager implements ChunkManager {
 public class HashChunkManager implements ChunkManager {
@@ -21,7 +25,10 @@ public class HashChunkManager implements ChunkManager {
         {
         {
             if (!chunkStore.isDirty())
             if (!chunkStore.isDirty())
                 continue;
                 continue;
-            writeChunkStore(Bukkit.getWorld(chunkStore.getWorldId()), chunkStore);
+            World world = Bukkit.getWorld(chunkStore.getWorldId());
+            if (world == null)
+                continue; // Oh well
+            writeChunkStore(world, chunkStore);
         }
         }
         // Clear in memory chunks
         // Clear in memory chunks
         chunkMap.clear();
         chunkMap.clear();
@@ -32,7 +39,7 @@ public class HashChunkManager implements ChunkManager {
         regionMap.clear();
         regionMap.clear();
     }
     }
 
 
-    private synchronized ChunkStore readChunkStore(World world, int cx, int cz) throws IOException {
+    private synchronized @Nullable ChunkStore readChunkStore(@NotNull World world, int cx, int cz) throws IOException {
         McMMOSimpleRegionFile rf = getSimpleRegionFile(world, cx, cz, false);
         McMMOSimpleRegionFile rf = getSimpleRegionFile(world, cx, cz, false);
         if (rf == null)
         if (rf == null)
             return null; // If there is no region file, there can't be a chunk
             return null; // If there is no region file, there can't be a chunk
@@ -43,7 +50,7 @@ public class HashChunkManager implements ChunkManager {
         }
         }
     }
     }
 
 
-    private synchronized void writeChunkStore(World world, ChunkStore data) {
+    private synchronized void writeChunkStore(@NotNull World world, @NotNull ChunkStore data) {
         if (!data.isDirty())
         if (!data.isDirty())
             return; // Don't save unchanged data
             return; // Don't save unchanged data
         try {
         try {
@@ -58,7 +65,7 @@ public class HashChunkManager implements ChunkManager {
         }
         }
     }
     }
 
 
-    private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int cx, int cz, boolean createIfAbsent) {
+    private synchronized @Nullable McMMOSimpleRegionFile getSimpleRegionFile(@NotNull World world, int cx, int cz, boolean createIfAbsent) {
         CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
         CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
 
 
         return regionMap.computeIfAbsent(regionKey, k -> {
         return regionMap.computeIfAbsent(regionKey, k -> {
@@ -73,7 +80,7 @@ public class HashChunkManager implements ChunkManager {
         });
         });
     }
     }
 
 
-    private ChunkStore loadChunk(int cx, int cz, World world) {
+    private @Nullable ChunkStore loadChunk(int cx, int cz, @NotNull World world) {
         try {
         try {
             return readChunkStore(world, cx, cz);
             return readChunkStore(world, cx, cz);
         }
         }
@@ -82,7 +89,7 @@ public class HashChunkManager implements ChunkManager {
         return null;
         return null;
     }
     }
 
 
-    private void unloadChunk(int cx, int cz, World world) {
+    private void unloadChunk(int cx, int cz, @NotNull World world) {
         CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
         CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
         ChunkStore chunkStore = chunkMap.remove(chunkKey); // Remove from chunk map
         ChunkStore chunkStore = chunkMap.remove(chunkKey); // Remove from chunk map
         if (chunkStore == null)
         if (chunkStore == null)
@@ -102,56 +109,12 @@ public class HashChunkManager implements ChunkManager {
     }
     }
 
 
     @Override
     @Override
-    public synchronized void saveChunk(int cx, int cz, World world) {
-        if (world == null)
-            return;
-
-        CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
-
-        ChunkStore out = chunkMap.get(chunkKey);
-
-        if (out == null)
-            return;
-
-        if (!out.isDirty())
-            return;
-
-        writeChunkStore(world, out);
-    }
-
-    @Override
-    public synchronized void chunkUnloaded(int cx, int cz, World world) {
-        if (world == null)
-            return;
-
+    public synchronized void chunkUnloaded(int cx, int cz, @NotNull World world) {
         unloadChunk(cx, cz, world);
         unloadChunk(cx, cz, world);
     }
     }
 
 
     @Override
     @Override
-    public synchronized void saveWorld(World world) {
-        if (world == null)
-            return;
-
-        UUID wID = world.getUID();
-
-        // Save all teh chunks
-        for (ChunkStore chunkStore : chunkMap.values()) {
-            if (!chunkStore.isDirty())
-                continue;
-            if (!wID.equals(chunkStore.getWorldId()))
-                continue;
-            try {
-                writeChunkStore(world, chunkStore);
-            }
-            catch (Exception ignore) { }
-        }
-    }
-
-    @Override
-    public synchronized void unloadWorld(World world) {
-        if (world == null)
-            return;
-
+    public synchronized void unloadWorld(@NotNull World world) {
         UUID wID = world.getUID();
         UUID wID = world.getUID();
 
 
         // Save and remove all the chunks
         // Save and remove all the chunks
@@ -177,18 +140,7 @@ public class HashChunkManager implements ChunkManager {
         }
         }
     }
     }
 
 
-    @Override
-    public synchronized void saveAll() {
-        for (World world : mcMMO.p.getServer().getWorlds()) {
-            saveWorld(world);
-        }
-    }
-
-    @Override
-    public synchronized boolean isTrue(int x, int y, int z, World world) {
-        if (world == null)
-            return false;
-
+    private synchronized boolean isTrue(int x, int y, int z, @NotNull World world) {
         CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
         CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
 
 
         // Get chunk, load from file if necessary
         // Get chunk, load from file if necessary
@@ -214,67 +166,36 @@ public class HashChunkManager implements ChunkManager {
     }
     }
 
 
     @Override
     @Override
-    public synchronized boolean isTrue(Block block) {
-        if (block == null)
-            return false;
-
+    public synchronized boolean isTrue(@NotNull Block block) {
         return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
         return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
     }
     }
 
 
     @Override
     @Override
-    public synchronized boolean isTrue(BlockState blockState) {
-        if (blockState == null)
-            return false;
-
+    public synchronized boolean isTrue(@NotNull BlockState blockState) {
         return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
         return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
     }
     }
 
 
     @Override
     @Override
-    public synchronized void setTrue(int x, int y, int z, World world) {
-        set(x, y, z, world, true);
-    }
-
-    @Override
-    public synchronized void setTrue(Block block) {
-        if (block == null)
-            return;
-
-        setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
+    public synchronized void setTrue(@NotNull Block block) {
+        set(block.getX(), block.getY(), block.getZ(), block.getWorld(), true);
     }
     }
 
 
     @Override
     @Override
-    public synchronized void setTrue(BlockState blockState) {
-        if (blockState == null)
-            return;
-
-        setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+    public synchronized void setTrue(@NotNull BlockState blockState) {
+        set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), true);
     }
     }
 
 
     @Override
     @Override
-    public synchronized void setFalse(int x, int y, int z, World world) {
-        set(x, y, z, world, false);
+    public synchronized void setFalse(@NotNull Block block) {
+        set(block.getX(), block.getY(), block.getZ(), block.getWorld(), false);
     }
     }
 
 
     @Override
     @Override
-    public synchronized void setFalse(Block block) {
-        if (block == null)
-            return;
-
-        setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
+    public synchronized void setFalse(@NotNull BlockState blockState) {
+        set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), false);
     }
     }
 
 
-    @Override
-    public synchronized void setFalse(BlockState blockState) {
-        if (blockState == null)
-            return;
-
-        setFalse(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
-    }
-
-    public synchronized  void set(int x, int y, int z, World world, boolean value){
-        if (world == null)
-            return;
-
+    private synchronized void set(int x, int y, int z, @NotNull World world, boolean value){
         CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
         CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
 
 
         // Get/Load/Create chunkstore
         // Get/Load/Create chunkstore
@@ -307,15 +228,15 @@ public class HashChunkManager implements ChunkManager {
         cStore.set(ix, y, iz, value);
         cStore.set(ix, y, iz, value);
     }
     }
 
 
-    private CoordinateKey blockCoordinateToChunkKey(UUID worldUid, int x, int y, int z) {
+    private CoordinateKey blockCoordinateToChunkKey(@NotNull UUID worldUid, int x, int y, int z) {
         return toChunkKey(worldUid, x >> 4, z >> 4);
         return toChunkKey(worldUid, x >> 4, z >> 4);
     }
     }
 
 
-    private CoordinateKey toChunkKey(UUID worldUid, int cx, int cz){
+    private CoordinateKey toChunkKey(@NotNull UUID worldUid, int cx, int cz){
         return new CoordinateKey(worldUid, cx, cz);
         return new CoordinateKey(worldUid, cx, cz);
     }
     }
 
 
-    private CoordinateKey toRegionKey(UUID worldUid, int cx, int cz) {
+    private CoordinateKey toRegionKey(@NotNull UUID worldUid, int cx, int cz) {
         // Compute region index (32x32 chunk regions)
         // Compute region index (32x32 chunk regions)
         int rx = cx >> 5;
         int rx = cx >> 5;
         int rz = cz >> 5;
         int rz = cz >> 5;
@@ -323,11 +244,11 @@ public class HashChunkManager implements ChunkManager {
     }
     }
 
 
     private static final class CoordinateKey {
     private static final class CoordinateKey {
-        public final UUID worldID;
+        public final @NotNull UUID worldID;
         public final int x;
         public final int x;
         public final int z;
         public final int z;
 
 
-        private CoordinateKey(UUID worldID, int x, int z) {
+        private CoordinateKey(@NotNull UUID worldID, int x, int z) {
             this.worldID = worldID;
             this.worldID = worldID;
             this.x = x;
             this.x = x;
             this.z = z;
             this.z = z;
@@ -348,7 +269,4 @@ public class HashChunkManager implements ChunkManager {
             return Objects.hash(worldID, x, z);
             return Objects.hash(worldID, x, z);
         }
         }
     }
     }
-
-    @Override
-    public synchronized void cleanUp() {}
 }
 }

+ 7 - 4
src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java

@@ -19,6 +19,9 @@
  */
  */
 package com.gmail.nossr50.util.blockmeta;
 package com.gmail.nossr50.util.blockmeta;
 
 
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
 import java.io.*;
 import java.io.*;
 import java.util.BitSet;
 import java.util.BitSet;
 import java.util.zip.DeflaterOutputStream;
 import java.util.zip.DeflaterOutputStream;
@@ -54,7 +57,7 @@ public class McMMOSimpleRegionFile {
     private final int segmentMask;
     private final int segmentMask;
 
 
     // File location
     // File location
-    private final File parent;
+    private final @NotNull File parent;
     // File access
     // File access
     private final RandomAccessFile file;
     private final RandomAccessFile file;
 
 
@@ -62,7 +65,7 @@ public class McMMOSimpleRegionFile {
     private final int rx;
     private final int rx;
     private final int rz;
     private final int rz;
 
 
-    public McMMOSimpleRegionFile(File f, int rx, int rz) {
+    public McMMOSimpleRegionFile(@NotNull File f, int rx, int rz) {
         this.rx = rx;
         this.rx = rx;
         this.rz = rz;
         this.rz = rz;
         this.parent = f;
         this.parent = f;
@@ -104,7 +107,7 @@ public class McMMOSimpleRegionFile {
         }
         }
     }
     }
 
 
-    public synchronized DataOutputStream getOutputStream(int x, int z) {
+    public synchronized @NotNull DataOutputStream getOutputStream(int x, int z) {
         int index = getChunkIndex(x, z); // Get chunk index
         int index = getChunkIndex(x, z); // Get chunk index
         return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
         return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
     }
     }
@@ -144,7 +147,7 @@ public class McMMOSimpleRegionFile {
         file.writeInt(chunkNumBytes[index]);
         file.writeInt(chunkNumBytes[index]);
     }
     }
 
 
-    public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
+    public synchronized @Nullable DataInputStream getInputStream(int x, int z) throws IOException {
         int index = getChunkIndex(x, z); // Get chunk index
         int index = getChunkIndex(x, z); // Get chunk index
         int byteLength = chunkNumBytes[index]; // Get byte length of data
         int byteLength = chunkNumBytes[index]; // Get byte length of data
 
 

+ 9 - 31
src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java

@@ -3,6 +3,7 @@ package com.gmail.nossr50.util.blockmeta;
 import org.bukkit.World;
 import org.bukkit.World;
 import org.bukkit.block.Block;
 import org.bukkit.block.Block;
 import org.bukkit.block.BlockState;
 import org.bukkit.block.BlockState;
+import org.jetbrains.annotations.NotNull;
 
 
 public class NullChunkManager implements ChunkManager {
 public class NullChunkManager implements ChunkManager {
 
 
@@ -10,53 +11,30 @@ public class NullChunkManager implements ChunkManager {
     public void closeAll() {}
     public void closeAll() {}
 
 
     @Override
     @Override
-    public void saveChunk(int cx, int cz, World world) {}
+    public void chunkUnloaded(int cx, int cz, @NotNull World world) {}
 
 
     @Override
     @Override
-    public void chunkUnloaded(int cx, int cz, World world) {}
+    public void unloadWorld(@NotNull World world) {}
 
 
     @Override
     @Override
-    public void saveWorld(World world) {}
-
-    @Override
-    public void unloadWorld(World world) {}
-
-    @Override
-    public void saveAll() {}
-
-    @Override
-    public boolean isTrue(int x, int y, int z, World world) {
-        return false;
-    }
-
-    @Override
-    public boolean isTrue(Block block) {
+    public boolean isTrue(@NotNull Block block) {
         return false;
         return false;
     }
     }
 
 
     @Override
     @Override
-    public boolean isTrue(BlockState blockState) {
+    public boolean isTrue(@NotNull BlockState blockState) {
         return false;
         return false;
     }
     }
 
 
     @Override
     @Override
-    public void setTrue(int x, int y, int z, World world) {}
-
-    @Override
-    public void setTrue(Block block) {}
-
-    @Override
-    public void setTrue(BlockState blockState) {}
-
-    @Override
-    public void setFalse(int x, int y, int z, World world) {}
+    public void setTrue(@NotNull Block block) {}
 
 
     @Override
     @Override
-    public void setFalse(Block block) {}
+    public void setTrue(@NotNull BlockState blockState) {}
 
 
     @Override
     @Override
-    public void setFalse(BlockState blockState) {}
+    public void setFalse(@NotNull Block block) {}
 
 
     @Override
     @Override
-    public void cleanUp() {}
+    public void setFalse(@NotNull BlockState blockState) {}
 }
 }

+ 56 - 0
src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java

@@ -0,0 +1,56 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import com.gmail.nossr50.mcMMO;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Contains blockstore methods that are safe for external plugins to access.
+ * An instance can be retrieved via {@link mcMMO#getPlaceStore() mcMMO.getPlaceStore()}
+ */
+public interface UserBlockTracker {
+    /**
+     * Check to see if a given block location is set to true
+     *
+     * @param block Block location to check
+     * @return true if the given block location is set to true, false if otherwise
+     */
+    boolean isTrue(@NotNull Block block);
+
+    /**
+     * Check to see if a given BlockState location is set to true
+     *
+     * @param blockState BlockState to check
+     * @return true if the given BlockState location is set to true, false if otherwise
+     */
+    boolean isTrue(@NotNull BlockState blockState);
+
+    /**
+     * Set a given block location to true
+     *
+     * @param block Block location to set
+     */
+    void setTrue(@NotNull Block block);
+
+    /**
+     * Set a given BlockState location to true
+     *
+     * @param blockState BlockState location to set
+     */
+    void setTrue(@NotNull BlockState blockState);
+
+    /**
+     * Set a given block location to false
+     *
+     * @param block Block location to set
+     */
+    void setFalse(@NotNull Block block);
+
+    /**
+     * Set a given BlockState location to false
+     *
+     * @param blockState BlockState location to set
+     */
+    void setFalse(@NotNull BlockState blockState);
+}

+ 7 - 1
src/main/java/com/gmail/nossr50/util/skills/PerksUtils.java

@@ -2,8 +2,12 @@ package com.gmail.nossr50.util.skills;
 
 
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.config.experience.ExperienceConfig;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.events.skills.SkillActivationPerkEvent;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.Permissions;
+import com.gmail.nossr50.util.player.UserManager;
+
+import org.bukkit.Bukkit;
 import org.bukkit.ChatColor;
 import org.bukkit.ChatColor;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Player;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.NotNull;
@@ -43,7 +47,9 @@ public final class PerksUtils {
             ticks += 4;
             ticks += 4;
         }
         }
 
 
-        return ticks;
+        final SkillActivationPerkEvent skillActivationPerkEvent = new SkillActivationPerkEvent(player, ticks, maxTicks);
+        Bukkit.getPluginManager().callEvent(skillActivationPerkEvent);
+        return skillActivationPerkEvent.getTicks();
     }
     }
 
 
     public static float handleXpPerks(Player player, float xp, PrimarySkillType skill) {
     public static float handleXpPerks(Player player, float xp, PrimarySkillType skill) {

+ 0 - 44
src/main/resources/fishing_treasures.yml

@@ -207,50 +207,6 @@ Fishing:
         Amount: 1
         Amount: 1
         XP: 200
         XP: 200
         Rarity: LEGENDARY
         Rarity: LEGENDARY
-    MUSIC_DISC_BLOCKS:
-        Amount: 1
-        XP: 200
-        Rarity: UNCOMMON
-    MUSIC_DISC_CHIRP:
-        Amount: 1
-        XP: 200
-        Rarity: UNCOMMON
-    MUSIC_DISC_FAR:
-        Amount: 1
-        XP: 200
-        Rarity: RARE
-    MUSIC_DISC_MALL:
-        Amount: 1
-        XP: 200
-        Rarity: RARE
-    MUSIC_DISC_MELLOHI:
-        Amount: 1
-        XP: 200
-        Rarity: RARE
-    MUSIC_DISC_STAL:
-        Amount: 1
-        XP: 200
-        Rarity: EPIC
-    MUSIC_DISC_STRAD:
-        Amount: 1
-        XP: 200
-        Rarity: EPIC
-    MUSIC_DISC_WARD:
-        Amount: 1
-        XP: 200
-        Rarity: EPIC
-    MUSIC_DISC_11:
-        Amount: 1
-        XP: 200
-        Rarity: LEGENDARY
-    MUSIC_DISC_WAIT:
-        Amount: 1
-        XP: 200
-        Rarity: LEGENDARY
-    MUSIC_DISC_13:
-        Amount: 1
-        XP: 200
-        Rarity: MYTHIC
     NETHERITE_SWORD:
     NETHERITE_SWORD:
         Amount: 1
         Amount: 1
         XP: 200
         XP: 200

+ 0 - 1
src/main/resources/locale/locale_cs_CZ.properties

@@ -90,7 +90,6 @@ Fishing.SubSkill.Shake.Description=Vyklepni p\u0159edm\u011bty z p\u0159\u00ed\u
 Fishing.SubSkill.FishermansDiet.Name=Ryb\u00e1\u0159\u016fv apetit
 Fishing.SubSkill.FishermansDiet.Name=Ryb\u00e1\u0159\u016fv apetit
 Fishing.SubSkill.FishermansDiet.Description=Zlep\u0161uje dopl\u0148ov\u00e1n\u00ed hladu z naryba\u0159en\u00fdch j\u00eddel
 Fishing.SubSkill.FishermansDiet.Description=Zlep\u0161uje dopl\u0148ov\u00e1n\u00ed hladu z naryba\u0159en\u00fdch j\u00eddel
 Fishing.SubSkill.MasterAngler.Name=Mistr Ryb\u00e1\u0159
 Fishing.SubSkill.MasterAngler.Name=Mistr Ryb\u00e1\u0159
-Fishing.SubSkill.MasterAngler.Description=Zvy\u0161uje \u0161anci zah\u00e1knut\u00ed ryby p\u0159i ryba\u0159en\u00ed
 Fishing.SubSkill.IceFishing.Name=Ryba\u0159en\u00ed v ledu
 Fishing.SubSkill.IceFishing.Name=Ryba\u0159en\u00ed v ledu
 Fishing.SubSkill.IceFishing.Description=Umo\u017e\u0148uje v\u00e1m ryba\u0159it v ledov\u00fdch prost\u0159ed\u00edch
 Fishing.SubSkill.IceFishing.Description=Umo\u017e\u0148uje v\u00e1m ryba\u0159it v ledov\u00fdch prost\u0159ed\u00edch
 Fishing.Chance.Raining=&9 De\u0161\u0165ov\u00fd bonus
 Fishing.Chance.Raining=&9 De\u0161\u0165ov\u00fd bonus

+ 0 - 1
src/main/resources/locale/locale_de.properties

@@ -372,7 +372,6 @@ Fishing.SubSkill.IceFishing.Stat            = Eisangeln
 Fishing.SubSkill.MagicHunter.Description    = Finde verzauberte Gegenst\u00E4nde
 Fishing.SubSkill.MagicHunter.Description    = Finde verzauberte Gegenst\u00E4nde
 Fishing.SubSkill.MagicHunter.Name           = Zauber J\u00E4ger
 Fishing.SubSkill.MagicHunter.Name           = Zauber J\u00E4ger
 Fishing.SubSkill.MagicHunter.Stat           = Zauber J\u00E4ger Chance
 Fishing.SubSkill.MagicHunter.Stat           = Zauber J\u00E4ger Chance
-Fishing.SubSkill.MasterAngler.Description   = Erh\u00F6ht die Chance des Anbei\u00DFens beim Angeln
 Fishing.SubSkill.MasterAngler.Name          = Superangel
 Fishing.SubSkill.MasterAngler.Name          = Superangel
 Fishing.SubSkill.Shake.Description          = Rei\u00DFe Gegenst\u00E4nde weg von Lebewesen und Spielern mit deiner Angel
 Fishing.SubSkill.Shake.Description          = Rei\u00DFe Gegenst\u00E4nde weg von Lebewesen und Spielern mit deiner Angel
 Fishing.SubSkill.Shake.Name                 = Rei\u00DFen
 Fishing.SubSkill.Shake.Name                 = Rei\u00DFen

+ 2 - 1
src/main/resources/locale/locale_en_US.properties

@@ -258,7 +258,7 @@ Fishing.SubSkill.FishermansDiet.Name=Fisherman's Diet
 Fishing.SubSkill.FishermansDiet.Description=Improves hunger restored from fished foods
 Fishing.SubSkill.FishermansDiet.Description=Improves hunger restored from fished foods
 Fishing.SubSkill.FishermansDiet.Stat=Fisherman's Diet:&a Rank {0}
 Fishing.SubSkill.FishermansDiet.Stat=Fisherman's Diet:&a Rank {0}
 Fishing.SubSkill.MasterAngler.Name=Master Angler
 Fishing.SubSkill.MasterAngler.Name=Master Angler
-Fishing.SubSkill.MasterAngler.Description=Fish are caught more frequently
+Fishing.SubSkill.MasterAngler.Description=Fish are caught more frequently, works better when fishing from a boat.
 Fishing.SubSkill.MasterAngler.Stat=Fishing min wait time reduction: &a-{0} seconds
 Fishing.SubSkill.MasterAngler.Stat=Fishing min wait time reduction: &a-{0} seconds
 Fishing.SubSkill.MasterAngler.Stat.Extra=Fishing max wait time reduction: &a-{0} seconds
 Fishing.SubSkill.MasterAngler.Stat.Extra=Fishing max wait time reduction: &a-{0} seconds
 Fishing.SubSkill.IceFishing.Name=Ice Fishing
 Fishing.SubSkill.IceFishing.Name=Ice Fishing
@@ -493,6 +493,7 @@ Taming.Summon.COTW.Success.WithoutLifespan=&a(Call Of The Wild) &7You have summo
 Taming.Summon.COTW.Success.WithLifespan=&a(Call Of The Wild) &7You have summoned a &6{0}&7 and it has a duration of &6{1}&7 seconds.
 Taming.Summon.COTW.Success.WithLifespan=&a(Call Of The Wild) &7You have summoned a &6{0}&7 and it has a duration of &6{1}&7 seconds.
 Taming.Summon.COTW.Limit=&a(Call Of The Wild) &7You can only have &c{0} &7summoned &7{1} pets at the same time.
 Taming.Summon.COTW.Limit=&a(Call Of The Wild) &7You can only have &c{0} &7summoned &7{1} pets at the same time.
 Taming.Summon.COTW.TimeExpired=&a(Call Of The Wild) &7Time is up, your &6{0}&7 departs.
 Taming.Summon.COTW.TimeExpired=&a(Call Of The Wild) &7Time is up, your &6{0}&7 departs.
+Taming.Summon.COTW.Removed=&a(Call Of The Wild) &7Your summoned &6{0}&7 has vanished from this world.
 Taming.Summon.COTW.BreedingDisallowed=&a(Call Of The Wild) &cYou cannot breed a summoned animal.
 Taming.Summon.COTW.BreedingDisallowed=&a(Call Of The Wild) &cYou cannot breed a summoned animal.
 Taming.Summon.COTW.NeedMoreItems=&a(Call Of The Wild) &7You need &e{0}&7 more &3{1}&7(s)
 Taming.Summon.COTW.NeedMoreItems=&a(Call Of The Wild) &7You need &e{0}&7 more &3{1}&7(s)
 Taming.Summon.Name.Format=&6(COTW) &f{0}'s {1}
 Taming.Summon.Name.Format=&6(COTW) &f{0}'s {1}

+ 0 - 1
src/main/resources/locale/locale_es.properties

@@ -91,7 +91,6 @@ Fishing.SubSkill.Shake.Description=Sacudir los items fuera de los monstruos con
 Fishing.SubSkill.FishermansDiet.Name=Dieta del pescador
 Fishing.SubSkill.FishermansDiet.Name=Dieta del pescador
 Fishing.SubSkill.FishermansDiet.Description=Mejora el hambre restaurada a partir de alimentos pescados
 Fishing.SubSkill.FishermansDiet.Description=Mejora el hambre restaurada a partir de alimentos pescados
 Fishing.SubSkill.MasterAngler.Name=Maestro pescador
 Fishing.SubSkill.MasterAngler.Name=Maestro pescador
-Fishing.SubSkill.MasterAngler.Description=Aumenta la probabilidad de ser mordido mientras se pesca
 Fishing.SubSkill.IceFishing.Name=Pesca de hielo
 Fishing.SubSkill.IceFishing.Name=Pesca de hielo
 Fishing.SubSkill.IceFishing.Description=Te permite pescar en biomas de hielo
 Fishing.SubSkill.IceFishing.Description=Te permite pescar en biomas de hielo
 Fishing.Chance.Raining=&9 Lluvia de Bonus
 Fishing.Chance.Raining=&9 Lluvia de Bonus

+ 0 - 1
src/main/resources/locale/locale_fr.properties

@@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=R\u00e9gime de fermier
 Fishing.SubSkill.FishermansDiet.Description=Am\u00e9liore la nutrition des produits de la ferme
 Fishing.SubSkill.FishermansDiet.Description=Am\u00e9liore la nutrition des produits de la ferme
 Fishing.SubSkill.FishermansDiet.Stat=R\u00e9gime de fermier:&a Grade {0}
 Fishing.SubSkill.FishermansDiet.Stat=R\u00e9gime de fermier:&a Grade {0}
 Fishing.SubSkill.MasterAngler.Name=Ma\u00eetre P\u00eacheur
 Fishing.SubSkill.MasterAngler.Name=Ma\u00eetre P\u00eacheur
-Fishing.SubSkill.MasterAngler.Description=Augmente les chances que \u00e7a morde lors de la p\u00eache
 Fishing.SubSkill.IceFishing.Name=P\u00eache sur Glace
 Fishing.SubSkill.IceFishing.Name=P\u00eache sur Glace
 Fishing.SubSkill.IceFishing.Description=Vous permet de p\u00eacher dans les biomes glac\u00e9s
 Fishing.SubSkill.IceFishing.Description=Vous permet de p\u00eacher dans les biomes glac\u00e9s
 Fishing.SubSkill.IceFishing.Stat=P\u00eache sur Glace
 Fishing.SubSkill.IceFishing.Stat=P\u00eache sur Glace

+ 0 - 1
src/main/resources/locale/locale_hu_HU.properties

@@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=Horg\u00E1szok Di\u00E9t\u00E1ja
 Fishing.SubSkill.FishermansDiet.Description=N\u00F6veli a kihal\u00E1szott \u00E9telek t\u00E1p\u00E9rt\u00E9k\u00E9t
 Fishing.SubSkill.FishermansDiet.Description=N\u00F6veli a kihal\u00E1szott \u00E9telek t\u00E1p\u00E9rt\u00E9k\u00E9t
 Fishing.SubSkill.FishermansDiet.Stat=Horg\u00E1szok Di\u00E9t\u00E1ja:&a Szint {0}
 Fishing.SubSkill.FishermansDiet.Stat=Horg\u00E1szok Di\u00E9t\u00E1ja:&a Szint {0}
 Fishing.SubSkill.MasterAngler.Name=Mester Horg\u00E1sz
 Fishing.SubSkill.MasterAngler.Name=Mester Horg\u00E1sz
-Fishing.SubSkill.MasterAngler.Description=N\u00F6veli a Kap\u00E1s es\u00E9ly\u00E9t horg\u00E1szat k\u00F6zben
 Fishing.SubSkill.IceFishing.Name=J\u00E9g Horg\u00E1szat
 Fishing.SubSkill.IceFishing.Name=J\u00E9g Horg\u00E1szat
 Fishing.SubSkill.IceFishing.Description=Lehet\u0151v\u00E9 teszi sz\u00E1modra, hogy fagyos t\u00E1jakon is horg\u00E1szhass
 Fishing.SubSkill.IceFishing.Description=Lehet\u0151v\u00E9 teszi sz\u00E1modra, hogy fagyos t\u00E1jakon is horg\u00E1szhass
 Fishing.SubSkill.IceFishing.Stat=J\u00E9g Horg\u00E1szat
 Fishing.SubSkill.IceFishing.Stat=J\u00E9g Horg\u00E1szat

+ 0 - 1
src/main/resources/locale/locale_it.properties

@@ -258,7 +258,6 @@ Fishing.SubSkill.FishermansDiet.Name=Dieta del Pescatore
 Fishing.SubSkill.FishermansDiet.Description=Aumenta la fame recuperata tramite cibi pescati
 Fishing.SubSkill.FishermansDiet.Description=Aumenta la fame recuperata tramite cibi pescati
 Fishing.SubSkill.FishermansDiet.Stat=Dieta del Pescatore:&a Grado {0}
 Fishing.SubSkill.FishermansDiet.Stat=Dieta del Pescatore:&a Grado {0}
 Fishing.SubSkill.MasterAngler.Name=Pescatore Provetto
 Fishing.SubSkill.MasterAngler.Name=Pescatore Provetto
-Fishing.SubSkill.MasterAngler.Description=Migliora la possibilit\u00E0 di ottenere un morso durante la pesca
 Fishing.SubSkill.IceFishing.Name=Pesca sul Ghiaccio
 Fishing.SubSkill.IceFishing.Name=Pesca sul Ghiaccio
 Fishing.SubSkill.IceFishing.Description=Ti permette di pescare in biomi ghiacciati
 Fishing.SubSkill.IceFishing.Description=Ti permette di pescare in biomi ghiacciati
 Fishing.SubSkill.IceFishing.Stat=Pesca sul Ghiaccio
 Fishing.SubSkill.IceFishing.Stat=Pesca sul Ghiaccio

+ 0 - 1
src/main/resources/locale/locale_ja_JP.properties

@@ -241,7 +241,6 @@ Fishing.SubSkill.FishermansDiet.Name=\u6f01\u5e2b\u306e\u98df\u4e8b
 Fishing.SubSkill.FishermansDiet.Description=\u9b5a\u4ecb\u985e\u304b\u3089\u56de\u5fa9\u3059\u308b\u6e80\u8179\u5ea6\u3092\u6539\u5584\u3059\u308b\u3002
 Fishing.SubSkill.FishermansDiet.Description=\u9b5a\u4ecb\u985e\u304b\u3089\u56de\u5fa9\u3059\u308b\u6e80\u8179\u5ea6\u3092\u6539\u5584\u3059\u308b\u3002
 Fishing.SubSkill.FishermansDiet.Stat=\u6f01\u5e2b\u306e\u98df\u4e8b:&a \u30e9\u30f3\u30af {0}
 Fishing.SubSkill.FishermansDiet.Stat=\u6f01\u5e2b\u306e\u98df\u4e8b:&a \u30e9\u30f3\u30af {0}
 Fishing.SubSkill.MasterAngler.Name=\u30de\u30b9\u30bf\u30fc\u30a2\u30f3\u30b0\u30e9\u30fc
 Fishing.SubSkill.MasterAngler.Name=\u30de\u30b9\u30bf\u30fc\u30a2\u30f3\u30b0\u30e9\u30fc
-Fishing.SubSkill.MasterAngler.Description=\u91e3\u308c\u308b\u78ba\u7387\u304c\u4e0a\u304c\u308a\u307e\u3059\u3002
 Fishing.SubSkill.IceFishing.Name=\u7a74\u91e3\u308a
 Fishing.SubSkill.IceFishing.Name=\u7a74\u91e3\u308a
 Fishing.SubSkill.IceFishing.Description=\u5bd2\u3044\u30d0\u30a4\u30aa\u30fc\u30e0\u3067\u306e\u91e3\u308a\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u306a\u308b\u3002
 Fishing.SubSkill.IceFishing.Description=\u5bd2\u3044\u30d0\u30a4\u30aa\u30fc\u30e0\u3067\u306e\u91e3\u308a\u304c\u3067\u304d\u308b\u3088\u3046\u306b\u306a\u308b\u3002
 Fishing.SubSkill.IceFishing.Stat=\u7a74\u91e3\u308a
 Fishing.SubSkill.IceFishing.Stat=\u7a74\u91e3\u308a

+ 0 - 1
src/main/resources/locale/locale_ko.properties

@@ -132,7 +132,6 @@ Fishing.SubSkill.Shake.Description=\uC544\uC774\uD15C\uC744 \uBAB9\uC774\uB098 \
 Fishing.SubSkill.FishermansDiet.Name=\uC5B4\uBD80\uC758 \uB2E4\uC774\uC5B4\uD2B8
 Fishing.SubSkill.FishermansDiet.Name=\uC5B4\uBD80\uC758 \uB2E4\uC774\uC5B4\uD2B8
 Fishing.SubSkill.FishermansDiet.Description=\uC5B4\uB958 \uC74C\uC2DD \uD5C8\uAE30 \uD68C\uBCF5 \uC99D\uAC00
 Fishing.SubSkill.FishermansDiet.Description=\uC5B4\uB958 \uC74C\uC2DD \uD5C8\uAE30 \uD68C\uBCF5 \uC99D\uAC00
 Fishing.SubSkill.MasterAngler.Name=\uB09A\uC2DC\uAFBC \uC7A5\uC778
 Fishing.SubSkill.MasterAngler.Name=\uB09A\uC2DC\uAFBC \uC7A5\uC778
-Fishing.SubSkill.MasterAngler.Description=\uB09A\uC2DC\uC911 \uC785\uC9C8 \uD655\uB960 \uC99D\uAC00
 Fishing.SubSkill.IceFishing.Name=\uC5BC\uC74C \uB09A\uC2DC
 Fishing.SubSkill.IceFishing.Name=\uC5BC\uC74C \uB09A\uC2DC
 Fishing.SubSkill.IceFishing.Description=\uC5BC\uC74C\uC774 \uB36E\uD600\uC788\uB294 \uD658\uACBD\uC5D0\uC11C \uB09A\uC2DC \uAC00\uB2A5
 Fishing.SubSkill.IceFishing.Description=\uC5BC\uC74C\uC774 \uB36E\uD600\uC788\uB294 \uD658\uACBD\uC5D0\uC11C \uB09A\uC2DC \uAC00\uB2A5
 Fishing.Chance.Raining=&9 \uBE44 \uD2B9\uD61C
 Fishing.Chance.Raining=&9 \uBE44 \uD2B9\uD61C

+ 0 - 1
src/main/resources/locale/locale_lt_LT.properties

@@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=Fisherman's Diet
 Fishing.SubSkill.FishermansDiet.Description=Improves hunger restored from fished foods
 Fishing.SubSkill.FishermansDiet.Description=Improves hunger restored from fished foods
 Fishing.SubSkill.FishermansDiet.Stat=Fisherman's Diet:&a Rank {0}
 Fishing.SubSkill.FishermansDiet.Stat=Fisherman's Diet:&a Rank {0}
 Fishing.SubSkill.MasterAngler.Name=Master Angler
 Fishing.SubSkill.MasterAngler.Name=Master Angler
-Fishing.SubSkill.MasterAngler.Description=Improves chance of getting a bite while fishing
 Fishing.SubSkill.IceFishing.Name=Ice Fishing
 Fishing.SubSkill.IceFishing.Name=Ice Fishing
 Fishing.SubSkill.IceFishing.Description=Allows you to fish in icy biomes
 Fishing.SubSkill.IceFishing.Description=Allows you to fish in icy biomes
 Fishing.SubSkill.IceFishing.Stat=Ice Fishing
 Fishing.SubSkill.IceFishing.Stat=Ice Fishing

+ 0 - 1
src/main/resources/locale/locale_nl.properties

@@ -71,7 +71,6 @@ Fishing.SubSkill.Shake.Description=Schud items af van mobs w/ hengel
 Fishing.SubSkill.FishermansDiet.Name=Visserman\'s dieet
 Fishing.SubSkill.FishermansDiet.Name=Visserman\'s dieet
 Fishing.SubSkill.FishermansDiet.Description=Verbetert de honger hersteld vanaf geviste voedingsmiddelen
 Fishing.SubSkill.FishermansDiet.Description=Verbetert de honger hersteld vanaf geviste voedingsmiddelen
 Fishing.SubSkill.MasterAngler.Name=Meester Hengelaar
 Fishing.SubSkill.MasterAngler.Name=Meester Hengelaar
-Fishing.SubSkill.MasterAngler.Description=Verbetert de kans op het bijten tijdens het vissen
 Fishing.SubSkill.IceFishing.Name=Ijs Vissen
 Fishing.SubSkill.IceFishing.Name=Ijs Vissen
 Fishing.SubSkill.IceFishing.Description=Stelt je in staat om te vissen in de ijzige biomen
 Fishing.SubSkill.IceFishing.Description=Stelt je in staat om te vissen in de ijzige biomen
 Fishing.Chance.Raining=&9 Regen Bonus
 Fishing.Chance.Raining=&9 Regen Bonus

+ 0 - 1
src/main/resources/locale/locale_pl.properties

@@ -89,7 +89,6 @@ Fishing.SubSkill.Shake.Name=Potrz\u0105\u015bni\u0119cie (przeciwko jednostkom)
 Fishing.SubSkill.Shake.Description=Okradaj potwory z przedmiot\u00f3w u\u017cywaj\u0105c w\u0119dki.
 Fishing.SubSkill.Shake.Description=Okradaj potwory z przedmiot\u00f3w u\u017cywaj\u0105c w\u0119dki.
 Fishing.SubSkill.FishermansDiet.Name=Dieta Rybaka
 Fishing.SubSkill.FishermansDiet.Name=Dieta Rybaka
 Fishing.SubSkill.FishermansDiet.Description=Zwi\u0119ksza nasycenie posi\u0142k\u00f3w (ryby)
 Fishing.SubSkill.FishermansDiet.Description=Zwi\u0119ksza nasycenie posi\u0142k\u00f3w (ryby)
-Fishing.SubSkill.MasterAngler.Description=Zwieksza szanse na zlapanie ryby na haczyk
 Fishing.SubSkill.IceFishing.Name=Lodowe lowienie ryb
 Fishing.SubSkill.IceFishing.Name=Lodowe lowienie ryb
 Fishing.SubSkill.IceFishing.Description=Pozwala na lowienie ryb w zimowych biomach
 Fishing.SubSkill.IceFishing.Description=Pozwala na lowienie ryb w zimowych biomach
 Fishing.Chance.Raining=&9 Bonus od Deszczu
 Fishing.Chance.Raining=&9 Bonus od Deszczu

+ 0 - 1
src/main/resources/locale/locale_ru.properties

@@ -199,7 +199,6 @@ Fishing.SubSkill.FishermansDiet.Name=\u0420\u044b\u0431\u0430\u0446\u043a\u0430\
 Fishing.SubSkill.FishermansDiet.Description=\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0443\u0442\u043e\u043b\u0435\u043d\u0438\u0435 \u0433\u043e\u043b\u043e\u0434\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0440\u044b\u0431\u0430\u0446\u043a\u043e\u0439 \u0435\u0434\u044b
 Fishing.SubSkill.FishermansDiet.Description=\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0443\u0442\u043e\u043b\u0435\u043d\u0438\u0435 \u0433\u043e\u043b\u043e\u0434\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0440\u044b\u0431\u0430\u0446\u043a\u043e\u0439 \u0435\u0434\u044b
 Fishing.SubSkill.FishermansDiet.Stat=\u0420\u044b\u0431\u0430\u0446\u043a\u0430\u044f \u0414\u0438\u0435\u0442\u0430:&a \u0420\u0430\u043d\u0433 {0}
 Fishing.SubSkill.FishermansDiet.Stat=\u0420\u044b\u0431\u0430\u0446\u043a\u0430\u044f \u0414\u0438\u0435\u0442\u0430:&a \u0420\u0430\u043d\u0433 {0}
 Fishing.SubSkill.MasterAngler.Name=\u041c\u0430\u0441\u0442\u0435\u0440 \u0420\u044b\u0431\u043e\u043b\u043e\u0432
 Fishing.SubSkill.MasterAngler.Name=\u041c\u0430\u0441\u0442\u0435\u0440 \u0420\u044b\u0431\u043e\u043b\u043e\u0432
-Fishing.SubSkill.MasterAngler.Description=\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0448\u0430\u043d\u0441 \u043f\u043e\u043a\u043b\u0435\u0432\u043a\u0438 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0440\u044b\u0431\u0430\u043b\u043a\u0438
 Fishing.SubSkill.IceFishing.Name=\u041f\u043e\u0434\u043b\u0435\u0434\u043d\u0430\u044f \u0420\u044b\u0431\u0430\u043b\u043a\u0430
 Fishing.SubSkill.IceFishing.Name=\u041f\u043e\u0434\u043b\u0435\u0434\u043d\u0430\u044f \u0420\u044b\u0431\u0430\u043b\u043a\u0430
 Fishing.SubSkill.IceFishing.Description=\u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u0430\u043c \u0440\u044b\u0431\u0430\u0447\u0438\u0442\u044c \u0432 \u0441\u043d\u0435\u0436\u043d\u044b\u0445 \u0431\u0438\u043e\u043c\u0430\u0445
 Fishing.SubSkill.IceFishing.Description=\u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u0430\u043c \u0440\u044b\u0431\u0430\u0447\u0438\u0442\u044c \u0432 \u0441\u043d\u0435\u0436\u043d\u044b\u0445 \u0431\u0438\u043e\u043c\u0430\u0445
 Fishing.SubSkill.IceFishing.Stat=\u041f\u043e\u0434\u043b\u0435\u0434\u043d\u0430\u044f \u0420\u044b\u0431\u0430\u043b\u043a\u0430
 Fishing.SubSkill.IceFishing.Stat=\u041f\u043e\u0434\u043b\u0435\u0434\u043d\u0430\u044f \u0420\u044b\u0431\u0430\u043b\u043a\u0430

+ 0 - 1
src/main/resources/locale/locale_th_TH.properties

@@ -90,7 +90,6 @@ Fishing.SubSkill.Shake.Description=\u0e40\u0e02\u0e22\u0e48\u0e32\u0e40\u0e2d\u0
 Fishing.SubSkill.FishermansDiet.Name=Fisherman\'s Diet
 Fishing.SubSkill.FishermansDiet.Name=Fisherman\'s Diet
 Fishing.SubSkill.FishermansDiet.Description=\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e04\u0e27\u0e32\u0e21\u0e2d\u0e34\u0e48\u0e21\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e01\u0e34\u0e19\u0e1b\u0e25\u0e32
 Fishing.SubSkill.FishermansDiet.Description=\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e04\u0e27\u0e32\u0e21\u0e2d\u0e34\u0e48\u0e21\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e01\u0e34\u0e19\u0e1b\u0e25\u0e32
 Fishing.SubSkill.MasterAngler.Name=Master Angler
 Fishing.SubSkill.MasterAngler.Name=Master Angler
-Fishing.SubSkill.MasterAngler.Description=\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e42\u0e2d\u0e01\u0e32\u0e2a\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e1b\u0e25\u0e32\u0e21\u0e32\u0e01\u0e02\u0e36\u0e49\u0e19\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e15\u0e01\u0e1b\u0e25\u0e32
 Fishing.SubSkill.IceFishing.Name=Ice Fishing
 Fishing.SubSkill.IceFishing.Name=Ice Fishing
 Fishing.SubSkill.IceFishing.Description=\u0e2d\u0e19\u0e38\u0e0d\u0e32\u0e15\u0e34\u0e43\u0e2b\u0e49\u0e15\u0e01\u0e1b\u0e25\u0e32\u0e43\u0e19\u0e19\u0e49\u0e33\u0e41\u0e02\u0e47\u0e07
 Fishing.SubSkill.IceFishing.Description=\u0e2d\u0e19\u0e38\u0e0d\u0e32\u0e15\u0e34\u0e43\u0e2b\u0e49\u0e15\u0e01\u0e1b\u0e25\u0e32\u0e43\u0e19\u0e19\u0e49\u0e33\u0e41\u0e02\u0e47\u0e07
 Fishing.Chance.Raining=&9 Rain Bonus
 Fishing.Chance.Raining=&9 Rain Bonus

+ 0 - 1
src/main/resources/locale/locale_zh_CN.properties

@@ -251,7 +251,6 @@ Fishing.SubSkill.FishermansDiet.Name=\u6e14\u592b\u7684\u98df\u8c31
 Fishing.SubSkill.FishermansDiet.Description=\u63d0\u9ad8\u9c7c\u7c7b\u98df\u7269\u6062\u590d\u7684\u9971\u98df\u5ea6
 Fishing.SubSkill.FishermansDiet.Description=\u63d0\u9ad8\u9c7c\u7c7b\u98df\u7269\u6062\u590d\u7684\u9971\u98df\u5ea6
 Fishing.SubSkill.FishermansDiet.Stat=\u6e14\u592b\u7684\u98df\u8c31:&a \u7b49\u7ea7 {0}
 Fishing.SubSkill.FishermansDiet.Stat=\u6e14\u592b\u7684\u98df\u8c31:&a \u7b49\u7ea7 {0}
 Fishing.SubSkill.MasterAngler.Name=\u9493\u9c7c\u5927\u5e08
 Fishing.SubSkill.MasterAngler.Name=\u9493\u9c7c\u5927\u5e08
-Fishing.SubSkill.MasterAngler.Description=\u63d0\u9ad8\u9493\u9c7c\u54ac\u94a9\u51e0\u7387
 Fishing.SubSkill.IceFishing.Name=\u51b0\u9493
 Fishing.SubSkill.IceFishing.Name=\u51b0\u9493
 Fishing.SubSkill.IceFishing.Description=\u5141\u8bb8\u4f60\u5728\u51b0\u51b7\u7684\u73af\u5883\u4e0b\u9493\u9c7c
 Fishing.SubSkill.IceFishing.Description=\u5141\u8bb8\u4f60\u5728\u51b0\u51b7\u7684\u73af\u5883\u4e0b\u9493\u9c7c
 Fishing.SubSkill.IceFishing.Stat=\u51b0\u9493
 Fishing.SubSkill.IceFishing.Stat=\u51b0\u9493

+ 0 - 1
src/main/resources/locale/locale_zh_TW.properties

@@ -93,7 +93,6 @@ Fishing.SubSkill.Shake.Description=\u7528\u91e3\u7aff\u628a\u602a\u7269\u7684\u7
 Fishing.SubSkill.FishermansDiet.Name=\u6f01\u4eba\u4fbf\u7576
 Fishing.SubSkill.FishermansDiet.Name=\u6f01\u4eba\u4fbf\u7576
 Fishing.SubSkill.FishermansDiet.Description=\u98df\u7528\u9b5a\u98df\u54c1\u6642\u984d\u5916\u6062\u5fa9\u98fd\u98df\u5ea6
 Fishing.SubSkill.FishermansDiet.Description=\u98df\u7528\u9b5a\u98df\u54c1\u6642\u984d\u5916\u6062\u5fa9\u98fd\u98df\u5ea6
 Fishing.SubSkill.MasterAngler.Name=\u5782\u91e3\u5927\u5e2b
 Fishing.SubSkill.MasterAngler.Name=\u5782\u91e3\u5927\u5e2b
-Fishing.SubSkill.MasterAngler.Description=\u589e\u52a0\u5728\u91e3\u9b5a\u6642\u4e0a\u9264\u7684\u6a5f\u7387
 Fishing.SubSkill.IceFishing.Name=\u51b0\u91e3
 Fishing.SubSkill.IceFishing.Name=\u51b0\u91e3
 Fishing.SubSkill.IceFishing.Description=\u5141\u8a31\u4f60\u5728\u51b0\u5929\u96ea\u5730\u88e1\u91e3\u9b5a
 Fishing.SubSkill.IceFishing.Description=\u5141\u8a31\u4f60\u5728\u51b0\u5929\u96ea\u5730\u88e1\u91e3\u9b5a
 Fishing.Chance.Raining=&9 \u5927\u91cf\u734e\u52f5
 Fishing.Chance.Raining=&9 \u5927\u91cf\u734e\u52f5

+ 29 - 16
src/test/java/ChunkStoreTest.java

@@ -2,6 +2,9 @@ import com.gmail.nossr50.util.blockmeta.*;
 import com.google.common.io.Files;
 import com.google.common.io.Files;
 import org.bukkit.Bukkit;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
 import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.junit.*;
 import org.junit.*;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.mockito.Mockito;
@@ -140,16 +143,27 @@ public class ChunkStoreTest {
     @Test
     @Test
     public void testRegressionChunkMirrorBug() {
     public void testRegressionChunkMirrorBug() {
         ChunkManager chunkManager = new HashChunkManager();
         ChunkManager chunkManager = new HashChunkManager();
-        chunkManager.setTrue(15,0,15, mockWorld);
-        chunkManager.setFalse(-15, 0, -15, mockWorld);
-        Assert.assertTrue(chunkManager.isTrue(15, 0, 15, mockWorld));
+        Block mockBlockA = mock(Block.class);
+        Mockito.when(mockBlockA.getX()).thenReturn(15);
+        Mockito.when(mockBlockA.getZ()).thenReturn(15);
+        Mockito.when(mockBlockA.getY()).thenReturn(0);
+        Mockito.when(mockBlockA.getWorld()).thenReturn(mockWorld);
+        Block mockBlockB = mock(Block.class);
+        Mockito.when(mockBlockB.getX()).thenReturn(-15);
+        Mockito.when(mockBlockB.getZ()).thenReturn(-15);
+        Mockito.when(mockBlockB.getY()).thenReturn(0);
+        Mockito.when(mockBlockB.getWorld()).thenReturn(mockWorld);
+
+        chunkManager.setTrue(mockBlockA);
+        chunkManager.setFalse(mockBlockB);
+        Assert.assertTrue(chunkManager.isTrue(mockBlockA));
     }
     }
 
 
     private interface Delegate {
     private interface Delegate {
         void run();
         void run();
     }
     }
 
 
-    private void assertThrows(Delegate delegate, Class<?> clazz) {
+    private void assertThrows(@NotNull Delegate delegate, @NotNull Class<?> clazz) {
         try {
         try {
             delegate.run();
             delegate.run();
             Assert.fail(); // We didn't throw
             Assert.fail(); // We didn't throw
@@ -170,7 +184,7 @@ public class ChunkStoreTest {
                     Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
                     Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
     }
     }
 
 
-    private static void recursiveDelete(File directoryToBeDeleted) {
+    private static void recursiveDelete(@NotNull File directoryToBeDeleted) {
         if (directoryToBeDeleted.isDirectory()) {
         if (directoryToBeDeleted.isDirectory()) {
             for (File file : directoryToBeDeleted.listFiles()) {
             for (File file : directoryToBeDeleted.listFiles()) {
                 recursiveDelete(file);
                 recursiveDelete(file);
@@ -179,7 +193,7 @@ public class ChunkStoreTest {
         directoryToBeDeleted.delete();
         directoryToBeDeleted.delete();
     }
     }
 
 
-    private static byte[] serializeChunkstore(ChunkStore chunkStore) throws IOException {
+    private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException {
         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         if (chunkStore instanceof BitSetChunkStore)
         if (chunkStore instanceof BitSetChunkStore)
             BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
             BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
@@ -188,18 +202,17 @@ public class ChunkStoreTest {
         return byteArrayOutputStream.toByteArray();
         return byteArrayOutputStream.toByteArray();
     }
     }
 
 
-
     public static class LegacyChunkStore implements ChunkStore, Serializable {
     public static class LegacyChunkStore implements ChunkStore, Serializable {
         private static final long serialVersionUID = -1L;
         private static final long serialVersionUID = -1L;
         transient private boolean dirty = false;
         transient private boolean dirty = false;
         public boolean[][][] store;
         public boolean[][][] store;
         private static final int CURRENT_VERSION = 7;
         private static final int CURRENT_VERSION = 7;
         private static final int MAGIC_NUMBER = 0xEA5EDEBB;
         private static final int MAGIC_NUMBER = 0xEA5EDEBB;
-        private int cx;
-        private int cz;
-        private UUID worldUid;
+        private final int cx;
+        private final int cz;
+        private final @NotNull UUID worldUid;
 
 
-        public LegacyChunkStore(World world, int cx, int cz) {
+        public LegacyChunkStore(@NotNull World world, int cx, int cz) {
             this.cx = cx;
             this.cx = cx;
             this.cz = cz;
             this.cz = cz;
             this.worldUid = world.getUID();
             this.worldUid = world.getUID();
@@ -227,7 +240,7 @@ public class ChunkStoreTest {
         }
         }
 
 
         @Override
         @Override
-        public UUID getWorldId() {
+        public @NotNull UUID getWorldId() {
             return worldUid;
             return worldUid;
         }
         }
 
 
@@ -274,7 +287,7 @@ public class ChunkStoreTest {
             return true;
             return true;
         }
         }
 
 
-        private void writeObject(ObjectOutputStream out) throws IOException {
+        private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
             out.writeInt(MAGIC_NUMBER);
             out.writeInt(MAGIC_NUMBER);
             out.writeInt(CURRENT_VERSION);
             out.writeInt(CURRENT_VERSION);
 
 
@@ -287,18 +300,18 @@ public class ChunkStoreTest {
             dirty = false;
             dirty = false;
         }
         }
 
 
-        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
             throw new UnsupportedOperationException();
             throw new UnsupportedOperationException();
         }
         }
     }
     }
 
 
     private static class UnitTestObjectOutputStream extends ObjectOutputStream {
     private static class UnitTestObjectOutputStream extends ObjectOutputStream {
-        public UnitTestObjectOutputStream(OutputStream outputStream) throws IOException {
+        public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
             super(outputStream);
             super(outputStream);
         }
         }
 
 
         @Override
         @Override
-        public void writeUTF(String str) throws IOException {
+        public void writeUTF(@NotNull String str) throws IOException {
             // Pretend to be the old class
             // Pretend to be the old class
             if (str.equals(LegacyChunkStore.class.getName()))
             if (str.equals(LegacyChunkStore.class.getName()))
                 str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
                 str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";