Forráskód Böngészése

update UserBlockTracker API

nossr50 1 éve
szülő
commit
8b82163e3d
26 módosított fájl, 700 hozzáadás és 460 törlés
  1. 8 0
      Changelog.txt
  2. 1 1
      pom.xml
  3. 0 1
      src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java
  4. 0 1
      src/main/java/com/gmail/nossr50/datatypes/skills/alchemy/PotionStage.java
  5. 5 5
      src/main/java/com/gmail/nossr50/listeners/BlockListener.java
  6. 4 5
      src/main/java/com/gmail/nossr50/listeners/EntityListener.java
  7. 1 1
      src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  8. 1 1
      src/main/java/com/gmail/nossr50/listeners/WorldListener.java
  9. 0 47
      src/main/java/com/gmail/nossr50/runnables/PistonTrackerTask.java
  10. 2 2
      src/main/java/com/gmail/nossr50/runnables/StickyPistonTrackerTask.java
  11. 0 2
      src/main/java/com/gmail/nossr50/runnables/skills/AlchemyBrewCheckTask.java
  12. 6 6
      src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java
  13. 2 2
      src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java
  14. 3 3
      src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java
  15. 2 2
      src/main/java/com/gmail/nossr50/util/BlockUtils.java
  16. 0 1
      src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java
  17. 19 9
      src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
  18. 16 6
      src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
  19. 96 6
      src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java
  20. 1 2
      src/test/java/com/gmail/nossr50/util/PotionEffectUtilTest.java
  21. 121 0
      src/test/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStoreTest.java
  22. 49 0
      src/test/java/com/gmail/nossr50/util/blockmeta/BlockStoreTestUtils.java
  23. 21 357
      src/test/java/com/gmail/nossr50/util/blockmeta/ChunkStoreTest.java
  24. 127 0
      src/test/java/com/gmail/nossr50/util/blockmeta/LegacyChunkStore.java
  25. 23 0
      src/test/java/com/gmail/nossr50/util/blockmeta/UnitTestObjectOutputStream.java
  26. 192 0
      src/test/java/com/gmail/nossr50/util/blockmeta/UserBlockTrackerTest.java

+ 8 - 0
Changelog.txt

@@ -1,3 +1,11 @@
+Version 2.2.013
+    (API) Added new methods to com.gmail.nossr50.util.blockmeta.UserBlockTracker for easier readability
+    (API) Deprecated the old poorly named methods in UserBlockTracker
+    (Codebase) Cleaned up and organized unit tests relating to UserBlockTracker
+
+    NOTES:
+    Not planning to delete the deprecated methods in UserBlockTracker anytime soon, as nothing has really changed other than the names
+
 Version 2.2.012
     Fixed a bug where Daze would cause an exception in older game versions (1.20.4 and older)
 

+ 1 - 1
pom.xml

@@ -2,7 +2,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.gmail.nossr50.mcMMO</groupId>
     <artifactId>mcMMO</artifactId>
-    <version>2.2.012</version>
+    <version>2.2.013-SNAPSHOT</version>
     <name>mcMMO</name>
     <url>https://github.com/mcMMO-Dev/mcMMO</url>
     <scm>

+ 0 - 1
src/main/java/com/gmail/nossr50/config/skills/alchemy/PotionConfig.java

@@ -4,7 +4,6 @@ import com.gmail.nossr50.config.LegacyConfigLoader;
 import com.gmail.nossr50.datatypes.skills.alchemy.AlchemyPotion;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.ItemUtils;
-import com.gmail.nossr50.util.PotionUtil;
 import org.bukkit.ChatColor;
 import org.bukkit.Color;
 import org.bukkit.Material;

+ 0 - 1
src/main/java/com/gmail/nossr50/datatypes/skills/alchemy/PotionStage.java

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.datatypes.skills.alchemy;
 
 import com.gmail.nossr50.util.PotionUtil;
-import org.bukkit.Bukkit;
 import org.bukkit.inventory.meta.PotionMeta;
 import org.bukkit.potion.PotionEffect;
 

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

@@ -310,7 +310,7 @@ public class BlockListener implements Listener {
 
         // 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 (BlockUtils.isWithinWorldBounds(block)) {
-            mcMMO.getPlaceStore().setFalse(block);
+            mcMMO.getPlaceStore().setEligible(block);
         }
     }
 
@@ -400,14 +400,14 @@ public class BlockListener implements Listener {
         else if (BlockUtils.affectedBySuperBreaker(blockState)
                 && (ItemUtils.isPickaxe(heldItem) || ItemUtils.isHoe(heldItem))
                 && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.MINING)
-                && !mcMMO.getPlaceStore().isTrue(blockState)) {
+                && !mcMMO.getPlaceStore().isIneligible(blockState)) {
             MiningManager miningManager = mcMMOPlayer.getMiningManager();
             miningManager.miningBlockCheck(blockState);
         }
 
         /* WOOD CUTTING */
         else if (BlockUtils.hasWoodcuttingXP(blockState) && ItemUtils.isAxe(heldItem)
-                && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.WOODCUTTING) && !mcMMO.getPlaceStore().isTrue(blockState)) {
+                && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.WOODCUTTING) && !mcMMO.getPlaceStore().isIneligible(blockState)) {
             WoodcuttingManager woodcuttingManager = mcMMOPlayer.getWoodcuttingManager();
             if (woodcuttingManager.canUseTreeFeller(heldItem)) {
                 woodcuttingManager.processTreeFeller(blockState);
@@ -422,7 +422,7 @@ public class BlockListener implements Listener {
         }
 
         /* EXCAVATION */
-        else if (BlockUtils.affectedByGigaDrillBreaker(blockState) && ItemUtils.isShovel(heldItem) && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.EXCAVATION) && !mcMMO.getPlaceStore().isTrue(blockState)) {
+        else if (BlockUtils.affectedByGigaDrillBreaker(blockState) && ItemUtils.isShovel(heldItem) && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.EXCAVATION) && !mcMMO.getPlaceStore().isIneligible(blockState)) {
             ExcavationManager excavationManager = mcMMOPlayer.getExcavationManager();
             excavationManager.excavationBlockCheck(blockState);
 
@@ -687,7 +687,7 @@ public class BlockListener implements Listener {
 
         if (UserManager.getPlayer(player).isDebugMode())
         {
-            if (mcMMO.getPlaceStore().isTrue(blockState))
+            if (mcMMO.getPlaceStore().isIneligible(blockState))
                 player.sendMessage("[mcMMO DEBUG] This block is not natural and does not reward treasures/XP");
             else
             {

+ 4 - 5
src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -44,7 +44,6 @@ import org.bukkit.metadata.FixedMetadataValue;
 import org.bukkit.potion.PotionEffect;
 import org.bukkit.potion.PotionEffectType;
 import org.bukkit.projectiles.ProjectileSource;
-import org.jetbrains.annotations.NotNull;
 
 import static com.gmail.nossr50.util.MobMetadataUtils.*;
 
@@ -207,8 +206,8 @@ public class EntityListener implements Listener {
         if (entity instanceof FallingBlock || entity instanceof Enderman) {
             boolean isTracked = entity.hasMetadata(MetadataConstants.METADATA_KEY_TRAVELING_BLOCK);
 
-            if (mcMMO.getPlaceStore().isTrue(block) && !isTracked) {
-                mcMMO.getPlaceStore().setFalse(block);
+            if (mcMMO.getPlaceStore().isIneligible(block) && !isTracked) {
+                mcMMO.getPlaceStore().setEligible(block);
 
                 entity.setMetadata(MetadataConstants.METADATA_KEY_TRAVELING_BLOCK, MetadataConstants.MCMMO_METADATA_VALUE);
                 TravelingBlockMetaCleanup metaCleanupTask = new TravelingBlockMetaCleanup(entity, pluginRef);
@@ -222,8 +221,8 @@ public class EntityListener implements Listener {
             //Redstone ore fire this event and should be ignored
         }
         else {
-            if (mcMMO.getPlaceStore().isTrue(block)) {
-                mcMMO.getPlaceStore().setFalse(block);
+            if (mcMMO.getPlaceStore().isIneligible(block)) {
+                mcMMO.getPlaceStore().setEligible(block);
             }
         }
     }

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

@@ -852,7 +852,7 @@ public class PlayerListener implements Listener {
                             case "NETHER_WART_BLOCK":
                             case "POTATO":
                             case "MANGROVE_PROPAGULE":
-                                mcMMO.getPlaceStore().setFalse(blockState);
+                                mcMMO.getPlaceStore().setEligible(blockState);
                                 break;
                         }
                     }

+ 1 - 1
src/main/java/com/gmail/nossr50/listeners/WorldListener.java

@@ -32,7 +32,7 @@ public class WorldListener implements Listener {
         // Using 50 ms later as I do not know of a way to run one tick later (safely)
         plugin.getFoliaLib().getImpl().runLater(() -> {
             for (BlockState blockState : event.getBlocks()) {
-                mcMMO.getPlaceStore().setFalse(blockState);
+                mcMMO.getPlaceStore().setEligible(blockState);
             }
         }, 1);
     }

+ 0 - 47
src/main/java/com/gmail/nossr50/runnables/PistonTrackerTask.java

@@ -1,47 +0,0 @@
-package com.gmail.nossr50.runnables;
-
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.BlockUtils;
-import com.gmail.nossr50.util.CancellableRunnable;
-import com.gmail.nossr50.util.MetadataConstants;
-import org.bukkit.block.Block;
-import org.bukkit.block.BlockFace;
-
-import java.util.List;
-
-public class PistonTrackerTask extends CancellableRunnable {
-    private final List<Block> blocks;
-    private final BlockFace direction;
-    private final Block futureEmptyBlock;
-
-    public PistonTrackerTask(List<Block> blocks, BlockFace direction, Block futureEmptyBlock) {
-        this.blocks = blocks;
-        this.direction = direction;
-        this.futureEmptyBlock = futureEmptyBlock;
-    }
-
-    @Override
-    public void run() {
-        // Check to see if futureEmptyBlock is empty - if it isn't; the blocks didn't move
-        if (!BlockUtils.isPistonPiece(futureEmptyBlock.getState())) {
-            return;
-        }
-
-        if (mcMMO.getPlaceStore().isTrue(futureEmptyBlock)) {
-            mcMMO.getPlaceStore().setFalse(futureEmptyBlock);
-        }
-
-        for (Block b : blocks) {
-            Block nextBlock = b.getRelative(direction);
-
-            if (nextBlock.hasMetadata(MetadataConstants.METADATA_KEY_PISTON_TRACKING)) {
-                BlockUtils.setUnnaturalBlock(nextBlock);
-                nextBlock.removeMetadata(MetadataConstants.METADATA_KEY_PISTON_TRACKING, mcMMO.p);
-            }
-            else if (mcMMO.getPlaceStore().isTrue(nextBlock)) {
-                // Block doesn't have metadatakey but isTrue - set it to false
-                mcMMO.getPlaceStore().setFalse(nextBlock);
-            }
-        }
-    }
-}

+ 2 - 2
src/main/java/com/gmail/nossr50/runnables/StickyPistonTrackerTask.java

@@ -19,7 +19,7 @@ public class StickyPistonTrackerTask extends CancellableRunnable {
 
     @Override
     public void run() {
-        if (!mcMMO.getPlaceStore().isTrue(movedBlock.getRelative(direction))) {
+        if (!mcMMO.getPlaceStore().isIneligible(movedBlock.getRelative(direction))) {
             return;
         }
 
@@ -29,7 +29,7 @@ public class StickyPistonTrackerTask extends CancellableRunnable {
         }
 
         // The sticky piston actually pulled the block so move the PlaceStore data
-        mcMMO.getPlaceStore().setFalse(movedBlock.getRelative(direction));
+        mcMMO.getPlaceStore().setEligible(movedBlock.getRelative(direction));
         BlockUtils.setUnnaturalBlock(movedBlock);
     }
 }

+ 0 - 2
src/main/java/com/gmail/nossr50/runnables/skills/AlchemyBrewCheckTask.java

@@ -2,7 +2,6 @@ package com.gmail.nossr50.runnables.skills;
 
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.skills.alchemy.Alchemy;
-import com.gmail.nossr50.skills.alchemy.AlchemyPotionBrewer;
 import com.gmail.nossr50.util.CancellableRunnable;
 import com.gmail.nossr50.util.ContainerMetadataUtils;
 import com.gmail.nossr50.util.player.UserManager;
@@ -17,7 +16,6 @@ import org.jetbrains.annotations.Nullable;
 import java.util.Arrays;
 
 import static com.gmail.nossr50.skills.alchemy.AlchemyPotionBrewer.isValidBrew;
-import static com.gmail.nossr50.util.EventUtils.getMcMMOPlayer;
 
 public class AlchemyBrewCheckTask extends CancellableRunnable {
     private final BrewingStand brewingStand;

+ 6 - 6
src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java

@@ -282,7 +282,7 @@ public class HerbalismManager extends SkillManager {
             if (brokenPlant.getLocation().equals(originalBreak.getBlock().getLocation())) {
                 //If its the same block as the original, we are going to directly check it for being a valid XP gain and add it to the nonChorusBlocks list even if its a chorus block
                 //This stops a delay from happening when bringing up the XP bar for chorus trees
-                if (!mcMMO.getPlaceStore().isTrue(originalBreak)) {
+                if (!mcMMO.getPlaceStore().isIneligible(originalBreak)) {
                     //Even if its a chorus block, the original break will be moved to nonChorusBlocks for immediate XP rewards
                     noDelayPlantBlocks.add(brokenPlant);
                 } else {
@@ -335,7 +335,7 @@ public class HerbalismManager extends SkillManager {
             BlockData plantData = brokenPlantState.getBlockData();
 
             //Check for double drops
-            if (!mcMMO.getPlaceStore().isTrue(brokenPlant)) {
+            if (!mcMMO.getPlaceStore().isIneligible(brokenPlant)) {
 
                 /*
                  *
@@ -413,7 +413,7 @@ public class HerbalismManager extends SkillManager {
             BlockState brokenBlockNewState = brokenPlantBlock.getState();
             BlockData plantData = brokenBlockNewState.getBlockData();
 
-            if (mcMMO.getPlaceStore().isTrue(brokenBlockNewState)) {
+            if (mcMMO.getPlaceStore().isIneligible(brokenBlockNewState)) {
                 /*
                  *
                  * Unnatural Blocks
@@ -427,7 +427,7 @@ public class HerbalismManager extends SkillManager {
                 }
 
                 //Mark it as natural again as it is being broken
-                mcMMO.getPlaceStore().setFalse(brokenBlockNewState);
+                mcMMO.getPlaceStore().setEligible(brokenBlockNewState);
             } else {
                 /*
                  *
@@ -489,9 +489,9 @@ public class HerbalismManager extends SkillManager {
                 continue;
             }
 
-            if (mcMMO.getPlaceStore().isTrue(brokenBlockNewState)) {
+            if (mcMMO.getPlaceStore().isIneligible(brokenBlockNewState)) {
                 //Mark it as natural again as it is being broken
-                mcMMO.getPlaceStore().setFalse(brokenBlockNewState);
+                mcMMO.getPlaceStore().setEligible(brokenBlockNewState);
             } else {
                 //TODO: Do we care about chorus flower age?
                 //Calculate XP for the old type

+ 2 - 2
src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java

@@ -181,7 +181,7 @@ public class MiningManager extends SkillManager {
             //Containers usually have 0 XP unless someone edited their config in a very strange way
             if (ExperienceConfig.getInstance().getXp(PrimarySkillType.MINING, targetBlock) != 0
                     && !(targetBlock instanceof Container)
-                    && !mcMMO.getPlaceStore().isTrue(targetBlock)) {
+                    && !mcMMO.getPlaceStore().isIneligible(targetBlock)) {
                 if (BlockUtils.isOre(blockState)) {
                     ores.add(blockState);
                 } else {
@@ -216,7 +216,7 @@ public class MiningManager extends SkillManager {
 
                 Misc.spawnItem(getPlayer(), Misc.getBlockCenter(blockState), new ItemStack(blockState.getType()), ItemSpawnReason.BLAST_MINING_ORES); // Initial block that would have been dropped
 
-                if (mcMMO.p.getAdvancedConfig().isBlastMiningBonusDropsEnabled() && !mcMMO.getPlaceStore().isTrue(blockState)) {
+                if (mcMMO.p.getAdvancedConfig().isBlastMiningBonusDropsEnabled() && !mcMMO.getPlaceStore().isIneligible(blockState)) {
                     for (int i = 1; i < dropMultiplier; i++) {
 //                        Bukkit.broadcastMessage("Bonus Drop on Ore: "+blockState.getType().toString());
                         Misc.spawnItem(getPlayer(), Misc.getBlockCenter(blockState), new ItemStack(blockState.getType()), ItemSpawnReason.BLAST_MINING_ORES_BONUS_DROP); // Initial block that would have been dropped

+ 3 - 3
src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java

@@ -113,7 +113,7 @@ public class WoodcuttingManager extends SkillManager {
     }
 
     public void processWoodcuttingBlockXP(@NotNull BlockState blockState) {
-        if (mcMMO.getPlaceStore().isTrue(blockState))
+        if (mcMMO.getPlaceStore().isIneligible(blockState))
             return;
 
         int xp = getExperienceFromLog(blockState);
@@ -269,7 +269,7 @@ public class WoodcuttingManager extends SkillManager {
      *     in treeFellerBlocks.
      */
     private boolean processTreeFellerTargetBlock(@NotNull BlockState blockState, @NotNull List<BlockState> futureCenterBlocks, @NotNull Set<BlockState> treeFellerBlocks) {
-        if (treeFellerBlocks.contains(blockState) || mcMMO.getPlaceStore().isTrue(blockState)) {
+        if (treeFellerBlocks.contains(blockState) || mcMMO.getPlaceStore().isIneligible(blockState)) {
             return false;
         }
 
@@ -373,7 +373,7 @@ public class WoodcuttingManager extends SkillManager {
      * @return Amount of experience
      */
     private static int processTreeFellerXPGains(BlockState blockState, int woodCount) {
-        if (mcMMO.getPlaceStore().isTrue(blockState))
+        if (mcMMO.getPlaceStore().isIneligible(blockState))
             return 0;
 
         int rawXP = ExperienceConfig.getInstance().getXp(PrimarySkillType.WOODCUTTING, blockState.getType());

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

@@ -65,7 +65,7 @@ public final class BlockUtils {
      * @param block target block
      */
     public static void setUnnaturalBlock(@NotNull Block block) {
-        mcMMO.getPlaceStore().setTrue(block);
+        mcMMO.getPlaceStore().setIneligible(block);
 
         // Failsafe against lingering metadata
         if (block.hasMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS))
@@ -82,7 +82,7 @@ public final class BlockUtils {
             block.removeMetadata(MetadataConstants.METADATA_KEY_REPLANT, mcMMO.p);
         }
 
-        mcMMO.getPlaceStore().setFalse(block);
+        mcMMO.getPlaceStore().setEligible(block);
     }
 
     /**

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

@@ -1,7 +1,6 @@
 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;

+ 19 - 9
src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java

@@ -152,7 +152,7 @@ public class HashChunkManager implements ChunkManager {
         }
     }
 
-    private synchronized boolean isTrue(int x, int y, int z, @NotNull World world) {
+    private synchronized boolean isIneligible(int x, int y, int z, @NotNull World world) {
         CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
 
         // Get chunk, load from file if necessary
@@ -178,32 +178,42 @@ public class HashChunkManager implements ChunkManager {
     }
 
     @Override
-    public synchronized boolean isTrue(@NotNull Block block) {
-        return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
+    public synchronized boolean isIneligible(@NotNull Block block) {
+        return isIneligible(block.getX(), block.getY(), block.getZ(), block.getWorld());
     }
 
     @Override
-    public synchronized boolean isTrue(@NotNull BlockState blockState) {
-        return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+    public synchronized boolean isIneligible(@NotNull BlockState blockState) {
+        return isIneligible(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
     }
 
     @Override
-    public synchronized void setTrue(@NotNull Block block) {
+    public synchronized boolean isEligible(@NotNull Block block) {
+        return !isIneligible(block);
+    }
+
+    @Override
+    public synchronized boolean isEligible(@NotNull BlockState blockState) {
+        return !isIneligible(blockState);
+    }
+
+    @Override
+    public synchronized void setIneligible(@NotNull Block block) {
         set(block.getX(), block.getY(), block.getZ(), block.getWorld(), true);
     }
 
     @Override
-    public synchronized void setTrue(@NotNull BlockState blockState) {
+    public synchronized void setIneligible(@NotNull BlockState blockState) {
         set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), true);
     }
 
     @Override
-    public synchronized void setFalse(@NotNull Block block) {
+    public synchronized void setEligible(@NotNull Block block) {
         set(block.getX(), block.getY(), block.getZ(), block.getWorld(), false);
     }
 
     @Override
-    public synchronized void setFalse(@NotNull BlockState blockState) {
+    public synchronized void setEligible(@NotNull BlockState blockState) {
         set(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld(), false);
     }
 

+ 16 - 6
src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java

@@ -17,24 +17,34 @@ public class NullChunkManager implements ChunkManager {
     public void unloadWorld(@NotNull World world) {}
 
     @Override
-    public boolean isTrue(@NotNull Block block) {
+    public boolean isIneligible(@NotNull Block block) {
         return false;
     }
 
     @Override
-    public boolean isTrue(@NotNull BlockState blockState) {
+    public boolean isIneligible(@NotNull BlockState blockState) {
         return false;
     }
 
     @Override
-    public void setTrue(@NotNull Block block) {}
+    public boolean isEligible(@NotNull Block block) {
+        return false;
+    }
+
+    @Override
+    public boolean isEligible(@NotNull BlockState blockState) {
+        return false;
+    }
+
+    @Override
+    public void setIneligible(@NotNull Block block) {}
 
     @Override
-    public void setTrue(@NotNull BlockState blockState) {}
+    public void setIneligible(@NotNull BlockState blockState) {}
 
     @Override
-    public void setFalse(@NotNull Block block) {}
+    public void setEligible(@NotNull Block block) {}
 
     @Override
-    public void setFalse(@NotNull BlockState blockState) {}
+    public void setEligible(@NotNull BlockState blockState) {}
 }

+ 96 - 6
src/main/java/com/gmail/nossr50/util/blockmeta/UserBlockTracker.java

@@ -10,47 +10,137 @@ import org.jetbrains.annotations.NotNull;
  * An instance can be retrieved via {@link mcMMO#getPlaceStore() mcMMO.getPlaceStore()}
  */
 public interface UserBlockTracker {
+    /**
+     * Check to see if a given {@link Block} is ineligible for rewards.
+     * This is a location-based lookup, and the other properties of the {@link Block} do not matter.
+     *
+     * @param block Block to check
+     * @return true if the given block should not give rewards, false if otherwise
+     */
+    boolean isIneligible(@NotNull Block block);
+
+    /**
+     * Check to see if a given {@link Block} is eligible for rewards.
+     * This is a location-based lookup, and the other properties of the {@link Block} do not matter.
+     *
+     * @param block Block to check
+     * @return true if the given block should give rewards, false if otherwise
+     */
+    boolean isEligible(@NotNull Block block);
+
+    /**
+     * Check to see if a given {@link BlockState} is eligible for rewards.
+     * This is a location-based lookup, and the other properties of the {@link BlockState} do not matter.
+     *
+     * @param blockState BlockState to check
+     * @return true if the given BlockState location is set to true, false if otherwise
+     */
+    boolean isEligible(@NotNull BlockState blockState);
+
+    /**
+     * Check to see if a given {@link BlockState} is ineligible for rewards.
+     * This is a location-based lookup, and the other properties of the {@link BlockState} do not matter.
+     *
+     * @param blockState BlockState to check
+     * @return true if the given BlockState location is set to true, false if otherwise
+     */
+    boolean isIneligible(@NotNull BlockState blockState);
+
+    /**
+     * Set a given {@link Block} as ineligible for rewards.
+     * This is a location-based lookup, and the other properties of the {@link Block} do not matter.
+     *
+     * @param block block whose location to set as ineligible
+     */
+    void setIneligible(@NotNull Block block);
+
+    /**
+     * Set a given BlockState location to true
+     *
+     * @param blockState BlockState location to set
+     */
+    void setIneligible(@NotNull BlockState blockState);
+
+    /**
+     * Set a given {@link Block} as eligible for rewards.
+     * This is a location-based lookup, and the other properties of the {@link Block} do not matter.
+     *
+     * @param block block whose location to set as eligible
+     */
+    void setEligible(@NotNull Block block);
+
+    /**
+     * Set a given BlockState location to false
+     *
+     * @param blockState BlockState location to set
+     */
+    void setEligible(@NotNull BlockState blockState);
+
     /**
      * 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
+     * @deprecated Use {@link #isIneligible(Block)} instead
      */
-    boolean isTrue(@NotNull Block block);
+    @Deprecated(since = "2.2.013")
+    default boolean isTrue(@NotNull Block block) {
+        return isIneligible(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
+     * @deprecated Use {@link #isIneligible(BlockState)} instead
      */
-    boolean isTrue(@NotNull BlockState blockState);
+    @Deprecated(since = "2.2.013")
+    default boolean isTrue(@NotNull BlockState blockState) {
+        return isIneligible(blockState);
+    }
 
     /**
      * Set a given block location to true
      *
      * @param block Block location to set
+     * @deprecated Use {@link #setIneligible(Block)} instead
      */
-    void setTrue(@NotNull Block block);
+    @Deprecated(since = "2.2.013")
+    default void setTrue(@NotNull Block block) {
+        setIneligible(block);
+    }
 
     /**
      * Set a given BlockState location to true
      *
      * @param blockState BlockState location to set
+     * @deprecated Use {@link #setIneligible(BlockState)} instead
      */
-    void setTrue(@NotNull BlockState blockState);
+    @Deprecated(since = "2.2.013")
+    default void setTrue(@NotNull BlockState blockState) {
+        setIneligible(blockState);
+    }
 
     /**
      * Set a given block location to false
      *
      * @param block Block location to set
+     * @deprecated Use {@link #setEligible(Block)} instead
      */
-    void setFalse(@NotNull Block block);
+    @Deprecated(since = "2.2.013")
+    default void setFalse(@NotNull Block block) {
+        setEligible(block);
+    }
 
     /**
      * Set a given BlockState location to false
      *
      * @param blockState BlockState location to set
+     * @deprecated Use {@link #setEligible(BlockState)} instead
      */
-    void setFalse(@NotNull BlockState blockState);
+    @Deprecated(since = "2.2.013")
+    default void setFalse(@NotNull BlockState blockState) {
+        setEligible(blockState);
+    }
 }

+ 1 - 2
src/test/java/com/gmail/nossr50/util/PotionEffectUtilTest.java

@@ -13,9 +13,8 @@ import org.mockito.MockedStatic;
 import static com.gmail.nossr50.util.PotionEffectUtil.getNauseaPotionEffectType;
 import static java.util.logging.Logger.getLogger;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.Mockito.*;
-import static org.mockito.Mockito.when;
 
 class PotionEffectUtilTest {
     private MockedStatic<mcMMO> mockedStaticMcMMO;

+ 121 - 0
src/test/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStoreTest.java

@@ -0,0 +1,121 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import com.gmail.nossr50.mcMMO;
+import com.google.common.io.Files;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.junit.jupiter.api.*;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.*;
+import static com.gmail.nossr50.util.blockmeta.UserBlockTrackerTest.recursiveDelete;
+import static org.bukkit.Bukkit.getWorld;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+class BitSetChunkStoreTest {
+    private static File tempDir;
+
+    @BeforeAll
+    public static void setUpClass() {
+        tempDir = Files.createTempDir();
+    }
+
+    @AfterAll
+    public static void tearDownClass() {
+        recursiveDelete(tempDir);
+    }
+
+    private World mockWorld;
+
+    private MockedStatic<Bukkit> bukkitMock;
+    private MockedStatic<mcMMO> mcMMOMock;
+
+    @BeforeEach
+    void setUpMock() {
+        UUID worldUUID = UUID.randomUUID();
+        mockWorld = Mockito.mock(World.class);
+        when(mockWorld.getUID()).thenReturn(worldUUID);
+        when(mockWorld.getMaxHeight()).thenReturn(256);
+        when(mockWorld.getWorldFolder()).thenReturn(tempDir);
+
+        bukkitMock = mockStatic(Bukkit.class);
+        bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
+
+        mcMMOMock = mockStatic(mcMMO.class);
+
+        when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
+        when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
+    }
+
+    @AfterEach
+    void teardownMock() {
+        bukkitMock.close();
+        mcMMOMock.close();
+    }
+
+    @Test
+    void testSetValue() {
+        final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
+        original.setTrue(0, 0, 0);
+        assertTrue(original.isTrue(0, 0, 0));
+        original.setFalse(0, 0, 0);
+        assertFalse(original.isTrue(0, 0, 0));
+    }
+
+    @Test
+    void testIsEmpty() {
+        final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
+        assertTrue(original.isEmpty());
+        original.setTrue(0, 0, 0);
+        original.setFalse(0, 0, 0);
+        assertTrue(original.isEmpty());
+    }
+
+    @Test
+    void testRoundTrip() throws IOException {
+        final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
+        original.setTrue(14, 89, 12);
+        original.setTrue(14, 90, 12);
+        original.setTrue(13, 89, 12);
+        byte[] serializedBytes = serializeChunkStore(original);
+        final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
+        assertChunkStoreEquals(original, deserialized);
+    }
+
+    @Test
+    void testNegativeWorldMin() throws IOException {
+        when(mockWorld.getMinHeight()).thenReturn(-64);
+
+        final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
+        original.setTrue(14, -32, 12);
+        original.setTrue(14, -64, 12);
+        original.setTrue(13, -63, 12);
+        byte[] serializedBytes = serializeChunkStore(original);
+        final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
+        assertChunkStoreEquals(original, deserialized);
+    }
+
+    @Test
+    void testNegativeWorldMinUpgrade() throws IOException {
+        final BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
+        original.setTrue(14, 1, 12);
+        original.setTrue(14, 2, 12);
+        original.setTrue(13, 3, 12);
+        byte[] serializedBytes = serializeChunkStore(original);
+
+        when(mockWorld.getMinHeight()).thenReturn(-64);
+        final ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
+        assert deserialized != null;
+        assertEqualIgnoreMinMax(original, deserialized);
+    }
+}

+ 49 - 0
src/test/java/com/gmail/nossr50/util/blockmeta/BlockStoreTestUtils.java

@@ -0,0 +1,49 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class BlockStoreTestUtils {
+    public static final int LEGACY_WORLD_HEIGHT_MAX = 256;
+    public static final int LEGACY_WORLD_HEIGHT_MIN = 0;
+
+    /**
+     * Asserts that the two ChunkStores are equal.
+     * @param expected The expected ChunkStore
+     * @param actual The actual ChunkStore
+     */
+    static void assertChunkStoreEquals(ChunkStore expected, ChunkStore actual) {
+        assertEquals(expected.getChunkMin(), actual.getChunkMin());
+        assertEquals(expected.getChunkMax(), actual.getChunkMax());
+        assertEqualIgnoreMinMax(expected, actual);
+    }
+
+    static byte[] serializeChunkStore(@NotNull ChunkStore chunkStore) throws IOException {
+        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        if (chunkStore instanceof BitSetChunkStore)
+            BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
+        else
+            new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if
+        // it were the old
+        // PrimitiveChunkStore
+        return byteArrayOutputStream.toByteArray();
+    }
+
+    static void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual) {
+        assertEquals(expected.getChunkX(), actual.getChunkX());
+        assertEquals(expected.getChunkZ(), actual.getChunkZ());
+        assertEquals(expected.getWorldId(), actual.getWorldId());
+        for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++) {
+            if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
+                continue; // Ignore
+            for (int x = 0; x < 16; x++)
+                for (int z = 0; z < 16; z++)
+                    assertEquals(expected.isTrue(x, y, z), actual.isTrue(x, y, z));
+        }
+    }
+}

+ 21 - 357
src/test/java/com/gmail/nossr50/util/blockmeta/ChunkStoreTest.java

@@ -1,13 +1,9 @@
 package com.gmail.nossr50.util.blockmeta;
 
-
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.BlockUtils;
 import com.google.common.io.Files;
 import org.bukkit.Bukkit;
 import org.bukkit.World;
-import org.bukkit.block.Block;
-import org.jetbrains.annotations.NotNull;
 import org.junit.jupiter.api.*;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
@@ -15,14 +11,13 @@ import org.mockito.Mockito;
 import java.io.*;
 import java.util.UUID;
 
-/**
- * Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies
- * that the serialization isn't completely broken.
- */
-class ChunkStoreTest {
+import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.*;
+import static com.gmail.nossr50.util.blockmeta.UserBlockTrackerTest.recursiveDelete;
+import static org.bukkit.Bukkit.getWorld;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
 
-    public static final int LEGACY_WORLD_HEIGHT_MAX = 256;
-    public static final int LEGACY_WORLD_HEIGHT_MIN = 0;
+class ChunkStoreTest {
     private static File tempDir;
 
     @BeforeAll
@@ -44,151 +39,39 @@ class ChunkStoreTest {
     void setUpMock() {
         UUID worldUUID = UUID.randomUUID();
         mockWorld = Mockito.mock(World.class);
-        Mockito.when(mockWorld.getUID()).thenReturn(worldUUID);
-        Mockito.when(mockWorld.getMaxHeight()).thenReturn(256);
-        Mockito.when(mockWorld.getWorldFolder()).thenReturn(tempDir);
+        when(mockWorld.getUID()).thenReturn(worldUUID);
+        when(mockWorld.getMaxHeight()).thenReturn(256);
+        when(mockWorld.getWorldFolder()).thenReturn(tempDir);
 
-        bukkitMock = Mockito.mockStatic(Bukkit.class);
-        bukkitMock.when(() -> Bukkit.getWorld(worldUUID)).thenReturn(mockWorld);
+        bukkitMock = mockStatic(Bukkit.class);
+        bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
 
-        mcMMOMock = Mockito.mockStatic(mcMMO.class);
+        mcMMOMock = mockStatic(mcMMO.class);
 
-        Mockito.when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
-        Mockito.when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
+        when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
+        when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
     }
-    
+
     @AfterEach
     void teardownMock() {
         bukkitMock.close();
         mcMMOMock.close();
     }
 
-    @Test
-    void testIndexOutOfBounds() {
-        Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
-        HashChunkManager hashChunkManager = new HashChunkManager();
-
-        // Top Block
-        Block illegalHeightBlock = initMockBlock(1337, 256, -1337);
-        Assertions.assertFalse(hashChunkManager.isTrue(illegalHeightBlock));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> hashChunkManager.setTrue(illegalHeightBlock));
-    }
-
-    @Test
-    void testSetTrue() {
-        Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
-        HashChunkManager hashChunkManager = new HashChunkManager();
-        int radius = 2; // Could be anything but drastically changes test time
-
-        for (int x = -radius; x <= radius; x++) {
-            for (int y = mockWorld.getMinHeight(); y < mockWorld.getMaxHeight(); y++) {
-                for (int z = -radius; z <= radius; z++) {
-                    Block testBlock = initMockBlock(x, y, z);
-
-                    hashChunkManager.setTrue(testBlock);
-                    Assertions.assertTrue(hashChunkManager.isTrue(testBlock));
-                    hashChunkManager.setFalse(testBlock);
-                    Assertions.assertFalse(hashChunkManager.isTrue(testBlock));
-                }
-            }
-        }
-
-        // Bot Block
-        Block bottomBlock = initMockBlock(1337, 0, -1337);
-        Assertions.assertFalse(hashChunkManager.isTrue(bottomBlock));
-
-        Assertions.assertTrue(BlockUtils.isWithinWorldBounds(bottomBlock));
-        hashChunkManager.setTrue(bottomBlock);
-        Assertions.assertTrue(hashChunkManager.isTrue(bottomBlock));
-
-        // Top Block
-        Block topBlock = initMockBlock(1337, 255, -1337);
-        Assertions.assertFalse(hashChunkManager.isTrue(topBlock));
-
-        Assertions.assertTrue(BlockUtils.isWithinWorldBounds(topBlock));
-        hashChunkManager.setTrue(topBlock);
-        Assertions.assertTrue(hashChunkManager.isTrue(topBlock));
-    }
-
-    @Test
-    void testSetValue() {
-        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
-        original.setTrue(0, 0, 0);
-        Assertions.assertTrue(original.isTrue(0, 0, 0));
-        original.setFalse(0, 0, 0);
-        Assertions.assertFalse(original.isTrue(0, 0, 0));
-    }
-
-    @Test
-    void testIsEmpty() {
-        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
-        Assertions.assertTrue(original.isEmpty());
-        original.setTrue(0, 0, 0);
-        original.setFalse(0, 0, 0);
-        Assertions.assertTrue(original.isEmpty());
-    }
-
-    @Test
-    void testRoundTrip() throws IOException {
-        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
-        original.setTrue(14, 89, 12);
-        original.setTrue(14, 90, 12);
-        original.setTrue(13, 89, 12);
-        byte[] serializedBytes = serializeChunkstore(original);
-        ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
-        assertEqual(original, deserialized);
-    }
-
-    @Test
-    void testNegativeWorldMin() throws IOException {
-        Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
-
-        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
-        original.setTrue(14, -32, 12);
-        original.setTrue(14, -64, 12);
-        original.setTrue(13, -63, 12);
-        byte[] serializedBytes = serializeChunkstore(original);
-        ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
-        assertEqual(original, deserialized);
-    }
-
-    @Test
-    void testNegativeWorldMinUpgrade() throws IOException {
-        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
-        original.setTrue(14, 1, 12);
-        original.setTrue(14, 2, 12);
-        original.setTrue(13, 3, 12);
-        byte[] serializedBytes = serializeChunkstore(original);
-
-        Mockito.when(mockWorld.getMinHeight()).thenReturn(-64);
-        ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
-        assert deserialized != null;
-        assertEqualIgnoreMinMax(original, deserialized);
-    }
-
-    @Test
-    void testChunkCoords() throws IOException {
-        for (int x = -96; x < 0; x++) {
-            int cx = x >> 4;
-            int ix = Math.abs(x) % 16;
-            //System.out.print(cx + ":" + ix + "  ");
-        }
-    }
-
     @Test
     void testUpgrade() throws IOException {
         LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 32);
         original.setTrue(14, 89, 12);
         original.setTrue(14, 90, 12);
         original.setTrue(13, 89, 12);
-        byte[] serializedBytes = serializeChunkstore(original);
+        byte[] serializedBytes = serializeChunkStore(original);
         ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
         assert deserialized != null;
-        assertEqual(original, deserialized);
+        assertChunkStoreEquals(original, deserialized);
     }
 
     @Test
-    void testSimpleRegionRoundtrip() throws IOException {
+    void testSimpleRegionRoundTrip() throws IOException {
         LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 12);
         original.setTrue(14, 89, 12);
         original.setTrue(14, 90, 12);
@@ -196,7 +79,7 @@ class ChunkStoreTest {
         File file = new File(tempDir, "SimpleRegionRoundTrip.region");
         McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
         try (DataOutputStream outputStream = region.getOutputStream(12, 12)) {
-            outputStream.write(serializeChunkstore(original));
+            outputStream.write(serializeChunkStore(original));
         }
         region.close();
         region = new McMMOSimpleRegionFile(file, 0, 0);
@@ -204,229 +87,10 @@ class ChunkStoreTest {
             Assertions.assertNotNull(is);
             ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(is);
             assert deserialized != null;
-            assertEqual(original, deserialized);
+            assertChunkStoreEquals(original, deserialized);
         }
         region.close();
         file.delete();
     }
 
-    @Test
-    void testSimpleRegionRejectsOutOfBounds() {
-        File file = new File(tempDir, "SimpleRegionRoundTrip.region");
-        McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(-1, 0));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, -1));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(32, 0));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, 32));
-        region.close();
-    }
-
-    @Test
-    void testChunkStoreRejectsOutOfBounds() {
-        ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(-1, 0, 0));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, -1, 0));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, -1));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(16, 0, 0));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0));
-        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, 16));
-    }
-
-    @Test
-    void testRegressionChunkMirrorBug() {
-        ChunkManager chunkManager = new HashChunkManager();
-        Block mockBlockA = Mockito.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 = Mockito.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);
-        Assertions.assertTrue(chunkManager.isTrue(mockBlockA));
-    }
-
-    private void assertEqual(ChunkStore expected, ChunkStore actual) {
-        Assertions.assertEquals(expected.getChunkMin(), actual.getChunkMin());
-        Assertions.assertEquals(expected.getChunkMax(), actual.getChunkMax());
-        assertEqualIgnoreMinMax(expected, actual);
-    }
-
-    private void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual) {
-        Assertions.assertEquals(expected.getChunkX(), actual.getChunkX());
-        Assertions.assertEquals(expected.getChunkZ(), actual.getChunkZ());
-        Assertions.assertEquals(expected.getWorldId(), actual.getWorldId());
-        for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++) {
-            if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
-                continue; // Ignore
-            for (int x = 0; x < 16; x++)
-                for (int z = 0; z < 16; z++)
-                    Assertions.assertEquals(expected.isTrue(x, y, z), actual.isTrue(x, y, z));
-        }
-    }
-
-    private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException {
-        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-        if (chunkStore instanceof BitSetChunkStore)
-            BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
-        else
-            new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if
-                                                                                           // it were the old
-                                                                                           // PrimitiveChunkStore
-        return byteArrayOutputStream.toByteArray();
-    }
-
-    public static class LegacyChunkStore implements ChunkStore, Serializable {
-        private static final long serialVersionUID = -1L;
-        transient private boolean dirty = false;
-        public boolean[][][] store;
-        private static final int CURRENT_VERSION = 7;
-        private static final int MAGIC_NUMBER = 0xEA5EDEBB;
-        private final int cx;
-        private final int cz;
-        private final @NotNull UUID worldUid;
-
-        public LegacyChunkStore(@NotNull World world, int cx, int cz) {
-            this.cx = cx;
-            this.cz = cz;
-            this.worldUid = world.getUID();
-            this.store = new boolean[16][16][world.getMaxHeight()];
-        }
-
-        @Override
-        public boolean isDirty() {
-            return dirty;
-        }
-
-        @Override
-        public void setDirty(boolean dirty) {
-            this.dirty = dirty;
-        }
-
-        @Override
-        public int getChunkX() {
-            return cx;
-        }
-
-        @Override
-        public int getChunkZ() {
-            return cz;
-        }
-
-        @Override
-        public int getChunkMin() {
-            return 0;
-        }
-
-        @Override
-        public int getChunkMax() {
-            return store[0][0].length;
-        }
-
-        @Override
-        public @NotNull UUID getWorldId() {
-            return worldUid;
-        }
-
-        @Override
-        public boolean isTrue(int x, int y, int z) {
-            return store[x][z][y];
-        }
-
-        @Override
-        public void setTrue(int x, int y, int z) {
-            if (y >= store[0][0].length || y < 0)
-                return;
-            store[x][z][y] = true;
-            dirty = true;
-        }
-
-        @Override
-        public void setFalse(int x, int y, int z) {
-            if (y >= store[0][0].length || y < 0)
-                return;
-            store[x][z][y] = false;
-            dirty = true;
-        }
-
-        @Override
-        public void set(int x, int y, int z, boolean value) {
-            if (y >= store[0][0].length || y < 0)
-                return;
-            store[x][z][y] = value;
-            dirty = true;
-        }
-
-        @Override
-        public boolean isEmpty() {
-            for (int x = 0; x < 16; x++) {
-                for (int z = 0; z < 16; z++) {
-                    for (int y = 0; y < store[0][0].length; y++) {
-                        if (store[x][z][y]) {
-                            return false;
-                        }
-                    }
-                }
-            }
-            return true;
-        }
-
-        private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
-            out.writeInt(MAGIC_NUMBER);
-            out.writeInt(CURRENT_VERSION);
-
-            out.writeLong(worldUid.getLeastSignificantBits());
-            out.writeLong(worldUid.getMostSignificantBits());
-            out.writeInt(cx);
-            out.writeInt(cz);
-            out.writeObject(store);
-
-            dirty = false;
-        }
-
-        private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
-            throw new UnsupportedOperationException();
-        }
-
-    }
-
-    private static class UnitTestObjectOutputStream extends ObjectOutputStream {
-
-        public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
-            super(outputStream);
-        }
-
-        @Override
-        public void writeUTF(@NotNull String str) throws IOException {
-            // Pretend to be the old class
-            if (str.equals(LegacyChunkStore.class.getName()))
-                str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
-            super.writeUTF(str);
-        }
-
-    }
-
-    @NotNull
-    private Block initMockBlock(int x, int y, int z) {
-        Block mockBlock = Mockito.mock(Block.class);
-        Mockito.when(mockBlock.getX()).thenReturn(x);
-        Mockito.when(mockBlock.getY()).thenReturn(y);
-        Mockito.when(mockBlock.getZ()).thenReturn(z);
-        Mockito.when(mockBlock.getWorld()).thenReturn(mockWorld);
-        return mockBlock;
-    }
-
-    public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
-        if (directoryToBeDeleted.isDirectory()) {
-            for (File file : directoryToBeDeleted.listFiles()) {
-                recursiveDelete(file);
-            }
-        }
-        directoryToBeDeleted.delete();
-    }
-}
+}

+ 127 - 0
src/test/java/com/gmail/nossr50/util/blockmeta/LegacyChunkStore.java

@@ -0,0 +1,127 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import org.bukkit.World;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * Used for unit testing upgrades from the old ChunkStore class.
+ */
+class LegacyChunkStore implements ChunkStore, Serializable {
+    private static final long serialVersionUID = -1L;
+    transient private boolean dirty = false;
+    public boolean[][][] store;
+    private static final int CURRENT_VERSION = 7;
+    private static final int MAGIC_NUMBER = 0xEA5EDEBB;
+    private final int cx;
+    private final int cz;
+    private final @NotNull UUID worldUid;
+
+    public LegacyChunkStore(@NotNull World world, int cx, int cz) {
+        this.cx = cx;
+        this.cz = cz;
+        this.worldUid = world.getUID();
+        this.store = new boolean[16][16][world.getMaxHeight()];
+    }
+
+    @Override
+    public boolean isDirty() {
+        return dirty;
+    }
+
+    @Override
+    public void setDirty(boolean dirty) {
+        this.dirty = dirty;
+    }
+
+    @Override
+    public int getChunkX() {
+        return cx;
+    }
+
+    @Override
+    public int getChunkZ() {
+        return cz;
+    }
+
+    @Override
+    public int getChunkMin() {
+        return 0;
+    }
+
+    @Override
+    public int getChunkMax() {
+        return store[0][0].length;
+    }
+
+    @Override
+    public @NotNull UUID getWorldId() {
+        return worldUid;
+    }
+
+    @Override
+    public boolean isTrue(int x, int y, int z) {
+        return store[x][z][y];
+    }
+
+    @Override
+    public void setTrue(int x, int y, int z) {
+        if (y >= store[0][0].length || y < 0)
+            return;
+        store[x][z][y] = true;
+        dirty = true;
+    }
+
+    @Override
+    public void setFalse(int x, int y, int z) {
+        if (y >= store[0][0].length || y < 0)
+            return;
+        store[x][z][y] = false;
+        dirty = true;
+    }
+
+    @Override
+    public void set(int x, int y, int z, boolean value) {
+        if (y >= store[0][0].length || y < 0)
+            return;
+        store[x][z][y] = value;
+        dirty = true;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        for (int x = 0; x < 16; x++) {
+            for (int z = 0; z < 16; z++) {
+                for (int y = 0; y < store[0][0].length; y++) {
+                    if (store[x][z][y]) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    private void writeObject(@NotNull ObjectOutputStream out) throws IOException {
+        out.writeInt(MAGIC_NUMBER);
+        out.writeInt(CURRENT_VERSION);
+
+        out.writeLong(worldUid.getLeastSignificantBits());
+        out.writeLong(worldUid.getMostSignificantBits());
+        out.writeInt(cx);
+        out.writeInt(cz);
+        out.writeObject(store);
+
+        dirty = false;
+    }
+
+    private void readObject(@NotNull ObjectInputStream in) throws IOException, ClassNotFoundException {
+        throw new UnsupportedOperationException();
+    }
+
+}

+ 23 - 0
src/test/java/com/gmail/nossr50/util/blockmeta/UnitTestObjectOutputStream.java

@@ -0,0 +1,23 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+
+class UnitTestObjectOutputStream extends ObjectOutputStream {
+
+    public UnitTestObjectOutputStream(@NotNull OutputStream outputStream) throws IOException {
+        super(outputStream);
+    }
+
+    @Override
+    public void writeUTF(@NotNull String str) throws IOException {
+        // Pretend to be the old class
+        if (str.equals(LegacyChunkStore.class.getName()))
+            str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
+        super.writeUTF(str);
+    }
+
+}

+ 192 - 0
src/test/java/com/gmail/nossr50/util/blockmeta/UserBlockTrackerTest.java

@@ -0,0 +1,192 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.util.BlockUtils;
+import com.google.common.io.Files;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.*;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.LEGACY_WORLD_HEIGHT_MAX;
+import static com.gmail.nossr50.util.blockmeta.BlockStoreTestUtils.LEGACY_WORLD_HEIGHT_MIN;
+import static org.bukkit.Bukkit.getWorld;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies
+ * that the serialization isn't completely broken.
+ */
+class UserBlockTrackerTest {
+    private static File tempDir;
+
+    @BeforeAll
+    public static void setUpClass() {
+        tempDir = Files.createTempDir();
+    }
+
+    @AfterAll
+    public static void tearDownClass() {
+        recursiveDelete(tempDir);
+    }
+
+    private World mockWorld;
+
+    private MockedStatic<Bukkit> bukkitMock;
+    private MockedStatic<mcMMO> mcMMOMock;
+
+    @BeforeEach
+    void setUpMock() {
+        UUID worldUUID = UUID.randomUUID();
+        mockWorld = Mockito.mock(World.class);
+        when(mockWorld.getUID()).thenReturn(worldUUID);
+        when(mockWorld.getMaxHeight()).thenReturn(256);
+        when(mockWorld.getWorldFolder()).thenReturn(tempDir);
+
+        bukkitMock = mockStatic(Bukkit.class);
+        bukkitMock.when(() -> getWorld(worldUUID)).thenReturn(mockWorld);
+
+        mcMMOMock = mockStatic(mcMMO.class);
+
+        when(mockWorld.getMinHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MIN);
+        when(mockWorld.getMaxHeight()).thenReturn(LEGACY_WORLD_HEIGHT_MAX);
+    }
+    
+    @AfterEach
+    void teardownMock() {
+        bukkitMock.close();
+        mcMMOMock.close();
+    }
+
+    @Test
+    void setIneligibleShouldThrowIndexOutOfBoundsException() {
+        when(mockWorld.getMinHeight()).thenReturn(-64);
+        final HashChunkManager hashChunkManager = new HashChunkManager();
+
+        // Top Block
+        final Block illegalHeightBlock = initMockBlock(1337, 256, -1337);
+        assertFalse(hashChunkManager.isIneligible(illegalHeightBlock));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> hashChunkManager.setIneligible(illegalHeightBlock));
+    }
+
+    @Test
+    void testSetEligibility() {
+        when(mockWorld.getMinHeight()).thenReturn(-64);
+        final HashChunkManager hashChunkManager = new HashChunkManager();
+        int radius = 2; // Could be anything but drastically changes test time
+
+        for (int x = -radius; x <= radius; x++) {
+            for (int y = mockWorld.getMinHeight(); y < mockWorld.getMaxHeight(); y++) {
+                for (int z = -radius; z <= radius; z++) {
+                    final Block testBlock = initMockBlock(x, y, z);
+                    // mark ineligible
+                    hashChunkManager.setIneligible(testBlock);
+                    assertTrue(hashChunkManager.isIneligible(testBlock));
+
+                    // mark eligible
+                    hashChunkManager.setEligible(testBlock);
+                    // Might as well test both isIneligible and isEligible while we are here
+                    assertFalse(hashChunkManager.isIneligible(testBlock));
+                    assertTrue(hashChunkManager.isEligible(testBlock));
+                }
+            }
+        }
+
+        // TODO: Whatever is going on down here should be in its own test
+        // Bot Block
+        final Block bottomBlock = initMockBlock(1337, 0, -1337);
+        assertFalse(hashChunkManager.isIneligible(bottomBlock));
+
+        assertTrue(BlockUtils.isWithinWorldBounds(bottomBlock));
+        hashChunkManager.setIneligible(bottomBlock);
+        assertTrue(hashChunkManager.isIneligible(bottomBlock));
+
+        // Top Block
+        final Block topBlock = initMockBlock(1337, 255, -1337);
+        assertFalse(hashChunkManager.isIneligible(topBlock));
+
+        assertTrue(BlockUtils.isWithinWorldBounds(topBlock));
+        hashChunkManager.setIneligible(topBlock);
+        assertTrue(hashChunkManager.isIneligible(topBlock));
+    }
+
+    @Test
+    void testChunkCoords() throws IOException {
+        // TODO: Unfinished test?
+        for (int x = -96; x < 0; x++) {
+            int cx = x >> 4;
+            int ix = Math.abs(x) % 16;
+            //System.out.print(cx + ":" + ix + "  ");
+        }
+    }
+
+    @Test
+    void testSimpleRegionRejectsOutOfBounds() {
+        File file = new File(tempDir, "SimpleRegionRoundTrip.region");
+        McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(-1, 0));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, -1));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(32, 0));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> region.getOutputStream(0, 32));
+        region.close();
+    }
+
+    @Test
+    void testChunkStoreRejectsOutOfBounds() {
+        ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(-1, 0, 0));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, -1, 0));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, -1));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(16, 0, 0));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0));
+        Assertions.assertThrows(IndexOutOfBoundsException.class, () -> chunkStore.setTrue(0, 0, 16));
+    }
+
+    @Test
+    void testRegressionChunkMirrorBug() {
+        final UserBlockTracker chunkManager = new HashChunkManager();
+        Block mockBlockA = Mockito.mock(Block.class);
+        when(mockBlockA.getX()).thenReturn(15);
+        when(mockBlockA.getZ()).thenReturn(15);
+        when(mockBlockA.getY()).thenReturn(0);
+        when(mockBlockA.getWorld()).thenReturn(mockWorld);
+        Block mockBlockB = Mockito.mock(Block.class);
+        when(mockBlockB.getX()).thenReturn(-15);
+        when(mockBlockB.getZ()).thenReturn(-15);
+        when(mockBlockB.getY()).thenReturn(0);
+        when(mockBlockB.getWorld()).thenReturn(mockWorld);
+
+        chunkManager.setIneligible(mockBlockA);
+        chunkManager.setEligible(mockBlockB);
+        assertTrue(chunkManager.isIneligible(mockBlockA));
+    }
+
+    @NotNull
+    private Block initMockBlock(int x, int y, int z) {
+        final Block mockBlock = Mockito.mock(Block.class);
+        when(mockBlock.getX()).thenReturn(x);
+        when(mockBlock.getY()).thenReturn(y);
+        when(mockBlock.getZ()).thenReturn(z);
+        when(mockBlock.getWorld()).thenReturn(mockWorld);
+        return mockBlock;
+    }
+
+    public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
+        if (directoryToBeDeleted.isDirectory()) {
+            for (File file : directoryToBeDeleted.listFiles()) {
+                recursiveDelete(file);
+            }
+        }
+        directoryToBeDeleted.delete();
+    }
+}