浏览代码

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

nossr50 4 年之前
父节点
当前提交
7ea3a2bf07

+ 4 - 0
pom.xml

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

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

@@ -253,12 +253,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);
     }
     }
 
 
     /**
     /**

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

@@ -345,7 +345,6 @@ public class mcMMO extends JavaPlugin {
 
 
             formulaManager.saveFormula();
             formulaManager.saveFormula();
             holidayManager.saveAnniversaryFiles();
             holidayManager.saveAnniversaryFiles();
-            placeStore.cleanUp();       // Cleanup empty metadata stores
             placeStore.closeAll();
             placeStore.closeAll();
         }
         }
 
 

+ 105 - 73
src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java

@@ -2,33 +2,37 @@ 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 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 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 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 +54,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
     }
     }
 
 
     @Override
     @Override
-    public UUID getWorldId() {
+    public @NotNull UUID getWorldId() {
         return worldUid;
         return worldUid;
     }
     }
 
 
@@ -81,58 +85,24 @@ 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(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(DataOutputStream out) throws IOException {
@@ -153,7 +123,7 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
         dirty = false;
         dirty = false;
     }
     }
 
 
-    private static BitSetChunkStore deserialize(DataInputStream in) throws IOException {
+    private static 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 +131,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 @NotNull ChunkStore readChunkStore(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 +174,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);
                 }
                 }
@@ -216,8 +194,61 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
             ((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 {
+            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(ObjectOutputStream out) throws IOException {
+                    throw new UnsupportedOperationException("You goofed.");
+                }
+
+                @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();
+
+                    store = (boolean[][][]) in.readObject();
+                    worldHeight = store[0][0].length;
+                }
+
+                public 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(InputStream in) throws IOException {
             public LegacyDeserializationInputStream(InputStream in) throws IOException {
                 super(in);
                 super(in);
                 enableResolveObject(true);
                 enableResolveObject(true);
@@ -227,13 +258,14 @@ public class BitSetChunkStore implements ChunkStore, Serializable {
             protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
             protected 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 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(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, 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);
+}

+ 17 - 4
src/test/java/ChunkStoreTest.java

@@ -2,6 +2,8 @@ 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.junit.*;
 import org.junit.*;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.mockito.Mockito;
@@ -140,9 +142,20 @@ 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 {
@@ -227,7 +240,7 @@ public class ChunkStoreTest {
         }
         }
 
 
         @Override
         @Override
-        public UUID getWorldId() {
+        public @NotNull UUID getWorldId() {
             return worldUid;
             return worldUid;
         }
         }