Browse Source

new chunkstore

Co-authored-by: t00thpick1 <t00thpick1dirko@gmail.com>
nossr50 4 years ago
parent
commit
01f31e76f5
28 changed files with 1210 additions and 2405 deletions
  1. 7 0
      Changelog.txt
  2. 20 2
      pom.xml
  3. 0 22
      src/main/java/com/gmail/nossr50/listeners/WorldListener.java
  4. 3 3
      src/main/java/com/gmail/nossr50/mcMMO.java
  5. 243 0
      src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java
  6. 1 71
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
  7. 1 1
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
  8. 15 10
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
  9. 0 151
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java
  10. 0 15
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java
  11. 0 48
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java
  12. 0 8
      src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java
  13. 354 0
      src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
  14. 0 410
      src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java
  15. 257 0
      src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java
  16. 1 41
      src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
  17. 0 85
      src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java
  18. 0 48
      src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java
  19. 0 180
      src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java
  20. 0 10
      src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java
  21. 0 447
      src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java
  22. 0 39
      src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java
  23. 0 306
      src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java
  24. 0 147
      src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java
  25. 0 90
      src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java
  26. 0 80
      src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java
  27. 0 191
      src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java
  28. 308 0
      src/test/java/ChunkStoreTest.java

+ 7 - 0
Changelog.txt

@@ -1,3 +1,10 @@
+Version 2.1.165
+    The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)
+    mcMMO will now be compatible with changes to world height (1.17 compatibility)
+
+    NOTES:
+    t00thpick1 has taken time to rewrite our block meta tracking system to be more efficient, easier to maintain, and support upcoming features such as world height changes
+
 Version 2.1.164
     mcMMO will now let players use vanilla blocks that have interactions (such as the vanilla Anvil) which are assigned as either Repair or Salvage blocks if a player is sneaking (see notes)
     The Rarity known as Records has been renamed to Mythic

+ 20 - 2
pom.xml

@@ -2,7 +2,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.gmail.nossr50.mcMMO</groupId>
     <artifactId>mcMMO</artifactId>
-    <version>2.1.164</version>
+    <version>2.1.165-SNAPSHOT</version>
     <name>mcMMO</name>
     <url>https://github.com/mcMMO-Dev/mcMMO</url>
     <scm>
@@ -279,7 +279,25 @@
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit-dep</artifactId>
-            <version>4.10</version>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.powermock</groupId>
+            <artifactId>powermock-module-junit4</artifactId>
+            <version>2.0.7</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.powermock</groupId>
+            <artifactId>powermock-api-mockito2</artifactId>
+            <version>2.0.7</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.4.6</version>
             <scope>test</scope>
         </dependency>
         <dependency>

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

@@ -42,28 +42,6 @@ public class WorldListener implements Listener {
         }
     }
 
-    /**
-     * Monitor WorldInit events.
-     *
-     * @param event The event to watch
-     */
-    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
-    public void onWorldInit(WorldInitEvent event) {
-        /* WORLD BLACKLIST CHECK */
-        if(WorldBlacklist.isWorldBlacklisted(event.getWorld()))
-            return;
-
-        World world = event.getWorld();
-
-        if (!new File(world.getWorldFolder(), "mcmmo_data").exists() || plugin == null) {
-            return;
-        }
-
-        plugin.getLogger().info("Converting block storage for " + world.getName() + " to a new format.");
-
-        //new BlockStoreConversionMain(world).run();
-    }
-
     /**
      * Monitor WorldUnload events.
      *

+ 3 - 3
src/main/java/com/gmail/nossr50/mcMMO.java

@@ -38,8 +38,8 @@ import com.gmail.nossr50.skills.salvage.salvageables.Salvageable;
 import com.gmail.nossr50.skills.salvage.salvageables.SalvageableManager;
 import com.gmail.nossr50.skills.salvage.salvageables.SimpleSalvageableManager;
 import com.gmail.nossr50.util.*;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManager;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManagerFactory;
+import com.gmail.nossr50.util.blockmeta.ChunkManager;
+import com.gmail.nossr50.util.blockmeta.ChunkManagerFactory;
 import com.gmail.nossr50.util.commands.CommandRegistrationManager;
 import com.gmail.nossr50.util.compat.CompatibilityManager;
 import com.gmail.nossr50.util.experience.FormulaManager;
@@ -336,8 +336,8 @@ public class mcMMO extends JavaPlugin {
 
             formulaManager.saveFormula();
             holidayManager.saveAnniversaryFiles();
-            placeStore.saveAll();       // Save our metadata
             placeStore.cleanUp();       // Cleanup empty metadata stores
+            placeStore.closeAll();
         }
 
         catch (Exception e) { e.printStackTrace(); }

+ 243 - 0
src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java

@@ -0,0 +1,243 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+
+import java.io.*;
+import java.util.BitSet;
+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;
+    private static final int CURRENT_VERSION = 8;
+    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) {
+        this.cx = cx;
+        this.cz = cz;
+        this.worldUid = world.getUID();
+        this.worldHeight = world.getMaxHeight();
+        this.store = new BitSet(16 * 16 * worldHeight);
+    }
+
+    private BitSetChunkStore() {}
+
+    @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 UUID getWorldId() {
+        return worldUid;
+    }
+
+    @Override
+    public boolean isTrue(int x, int y, int z) {
+        return store.get(coordToIndex(x, y, z));
+    }
+
+    @Override
+    public void setTrue(int x, int y, int z) {
+        set(x, y, z, true);
+    }
+
+    @Override
+    public void setFalse(int x, int y, int z) {
+        set(x, y, z, false);
+    }
+
+    @Override
+    public void set(int x, int y, int z, boolean value) {
+        store.set(coordToIndex(x, y, z), value);
+        dirty = true;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return store.isEmpty();
+    }
+
+    private int coordToIndex(int x, int y, int z) {
+        if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16)
+            throw new IndexOutOfBoundsException();
+        return (z * 16 + x) + (256 * y);
+    }
+
+    private void fixWorldHeight() {
+        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?
+        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");
+    }
+
+    @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();
+    }
+
+    private void serialize(DataOutputStream 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.writeInt(worldHeight);
+
+        // Store the byte array directly so we don't have the object type info overhead
+        byte[] storeData = store.toByteArray();
+        out.writeInt(storeData.length);
+        out.write(storeData);
+
+        dirty = false;
+    }
+
+    private static BitSetChunkStore deserialize(DataInputStream in) throws IOException {
+        int magic = in.readInt();
+        // Can be used to determine the format of the file
+        int fileVersionNumber = in.readInt();
+
+        if (magic != MAGIC_NUMBER || fileVersionNumber != CURRENT_VERSION)
+            throw new IOException();
+
+        BitSetChunkStore chunkStore = new BitSetChunkStore();
+
+        long lsb = in.readLong();
+        long msb = in.readLong();
+        chunkStore.worldUid = new UUID(msb, lsb);
+        chunkStore.cx = in.readInt();
+        chunkStore.cz = in.readInt();
+
+        chunkStore.worldHeight = in.readInt();
+        byte[] temp = new byte[in.readInt()];
+        in.readFully(temp);
+        chunkStore.store = BitSet.valueOf(temp);
+
+        chunkStore.fixWorldHeight();
+        return chunkStore;
+    }
+
+    public static class Serialization {
+
+        public static final short STREAM_MAGIC = (short)0xACDC;
+
+        public static ChunkStore readChunkStore(DataInputStream inputStream) throws IOException {
+            if (inputStream.markSupported())
+                inputStream.mark(2);
+            short magicNumber = inputStream.readShort();
+
+            if (magicNumber == ObjectStreamConstants.STREAM_MAGIC) // Java serializable, use legacy serialization
+            {
+                // "Un-read" the magic number for Serializables, they need it to still be in the stream
+                if (inputStream.markSupported())
+                    inputStream.reset(); // Pretend we never read those bytes
+                else
+                {
+                    // 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.unread((magicNumber >>> 0) & 0xFF);
+                    pushbackInputStream.unread((magicNumber >>> 8) & 0xFF);
+                    inputStream = new DataInputStream(pushbackInputStream);
+                }
+                return new LegacyDeserializationInputStream(inputStream).readLegacyChunkStore();
+            }
+            else if (magicNumber == STREAM_MAGIC) // Pure bytes format
+            {
+                return BitSetChunkStore.deserialize(inputStream);
+            }
+            throw new IOException("Bad Data Format");
+        }
+
+        public static void writeChunkStore(DataOutputStream outputStream, ChunkStore chunkStore) throws IOException {
+            if (!(chunkStore instanceof BitSetChunkStore))
+                throw new InvalidClassException("ChunkStore must be instance of BitSetChunkStore");
+            outputStream.writeShort(STREAM_MAGIC);
+            ((BitSetChunkStore)chunkStore).serialize(outputStream);
+        }
+
+        // Handles loading the old serialized classes even though we have changed name/package
+        private static class LegacyDeserializationInputStream extends ObjectInputStream {
+            public LegacyDeserializationInputStream(InputStream in) throws IOException {
+                super(in);
+                enableResolveObject(true);
+            }
+
+            @Override
+            protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
+                ObjectStreamClass read = super.readClassDescriptor();
+                if (read.getName().contentEquals("com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore"))
+                    return ObjectStreamClass.lookup(BitSetChunkStore.class);
+                return read;
+            }
+
+            public ChunkStore readLegacyChunkStore(){
+                try {
+                    return (ChunkStore) readObject();
+                } catch (IOException | ClassNotFoundException e) {
+                    return null;
+                }
+            }
+        }
+    }
+}

+ 1 - 71
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java → src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java

@@ -1,59 +1,12 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
 
 import org.bukkit.World;
 import org.bukkit.block.Block;
 import org.bukkit.block.BlockState;
-import org.bukkit.entity.Entity;
-
-import java.io.IOException;
 
 public interface ChunkManager {
     void closeAll();
 
-    ChunkStore readChunkStore(World world, int x, int z) throws IOException;
-
-    void writeChunkStore(World world, int x, int z, ChunkStore data);
-
-    void closeChunkStore(World world, int x, int z);
-
-    /**
-     * Loads a specific chunklet
-     *
-     * @param cx Chunklet X coordinate that needs to be loaded
-     * @param cy Chunklet Y coordinate that needs to be loaded
-     * @param cz Chunklet Z coordinate that needs to be loaded
-     * @param world World that the chunklet needs to be loaded in
-     */
-    void loadChunklet(int cx, int cy, int cz, World world);
-
-    /**
-     * Unload a specific chunklet
-     *
-     * @param cx Chunklet X coordinate that needs to be unloaded
-     * @param cy Chunklet Y coordinate that needs to be unloaded
-     * @param cz Chunklet Z coordinate that needs to be unloaded
-     * @param world World that the chunklet needs to be unloaded from
-     */
-    void unloadChunklet(int cx, int cy, int cz, World world);
-
-    /**
-     * Load a given Chunk's Chunklet data
-     *
-     * @param cx Chunk X coordinate that is to be loaded
-     * @param cz Chunk Z coordinate that is to be loaded
-     * @param world World that the Chunk is in
-     */
-    void loadChunk(int cx, int cz, World world, Entity[] entities);
-
-    /**
-     * Unload a given Chunk's Chunklet data
-     *
-     * @param cx Chunk X coordinate that is to be unloaded
-     * @param cz Chunk Z coordinate that is to be unloaded
-     * @param world World that the Chunk is in
-     */
-    void unloadChunk(int cx, int cz, World world);
-
     /**
      * Saves a given Chunk's Chunklet data
      *
@@ -63,17 +16,6 @@ public interface ChunkManager {
      */
     void saveChunk(int cx, int cz, World world);
 
-    boolean isChunkLoaded(int cx, int cz, World world);
-
-    /**
-     * Informs the ChunkletManager a chunk is loaded
-     *
-     * @param cx Chunk X coordinate that is loaded
-     * @param cz Chunk Z coordinate that is loaded
-     * @param world World that the chunk was loaded in
-     */
-    void chunkLoaded(int cx, int cz, World world);
-
     /**
      * Informs the ChunkletManager a chunk is unloaded
      *
@@ -97,23 +39,11 @@ public interface ChunkManager {
      */
     void unloadWorld(World world);
 
-    /**
-     * Load all ChunkletStores from all loaded chunks from this world into memory
-     *
-     * @param world World to load
-     */
-    void loadWorld(World world);
-
     /**
      * Save all ChunkletStores
      */
     void saveAll();
 
-    /**
-     * Unload all ChunkletStores after saving them
-     */
-    void unloadAll();
-
     /**
      * Check to see if a given location is set to true
      *

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

@@ -1,4 +1,4 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
 
 import com.gmail.nossr50.config.HiddenConfig;
 

+ 15 - 10
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java → src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java

@@ -1,13 +1,13 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
 
-import com.gmail.nossr50.util.blockmeta.ChunkletStore;
+import org.bukkit.World;
 
-import java.io.Serializable;
+import java.util.UUID;
 
 /**
  * A ChunkStore should be responsible for a 16x16xWorldHeight area of data
  */
-public interface ChunkStore extends Serializable {
+public interface ChunkStore {
     /**
      * Checks the chunk's save state
      *
@@ -36,6 +36,8 @@ public interface ChunkStore extends Serializable {
      */
     int getChunkZ();
 
+    UUID getWorldId();
+
     /**
      * Checks the value at the given coordinates
      *
@@ -65,14 +67,17 @@ public interface ChunkStore extends Serializable {
     void setFalse(int x, int y, int z);
 
     /**
-     * @return true if all values in the chunklet are false, false if otherwise
+     * Set the value at the given coordinates
+     *
+     * @param x x coordinate in current chunklet
+     * @param y y coordinate in current chunklet
+     * @param z z coordinate in current chunklet
+     * @param value value to set
      */
-    boolean isEmpty();
+    void set(int x, int y, int z, boolean value);
 
     /**
-     * Set all values in this ChunkletStore to the values from another provided ChunkletStore
-     *
-     * @param otherStore Another ChunkletStore that this one should copy all data from
+     * @return true if all values in the chunklet are false, false if otherwise
      */
-    void copyFrom(ChunkletStore otherStore);
+    boolean isEmpty();
 }

+ 0 - 151
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java

@@ -1,151 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import org.bukkit.World;
-import org.bukkit.block.Block;
-
-public interface ChunkletManager {
-    /**
-     * Loads a specific chunklet
-     *
-     * @param cx Chunklet X coordinate that needs to be loaded
-     * @param cy Chunklet Y coordinate that needs to be loaded
-     * @param cz Chunklet Z coordinate that needs to be loaded
-     * @param world World that the chunklet needs to be loaded in
-     */
-    void loadChunklet(int cx, int cy, int cz, World world);
-
-    /**
-     * Unload a specific chunklet
-     *
-     * @param cx Chunklet X coordinate that needs to be unloaded
-     * @param cy Chunklet Y coordinate that needs to be unloaded
-     * @param cz Chunklet Z coordinate that needs to be unloaded
-     * @param world World that the chunklet needs to be unloaded from
-     */
-    void unloadChunklet(int cx, int cy, int cz, World world);
-
-    /**
-     * Load a given Chunk's Chunklet data
-     *
-     * @param cx Chunk X coordinate that is to be loaded
-     * @param cz Chunk Z coordinate that is to be loaded
-     * @param world World that the Chunk is in
-     */
-    void loadChunk(int cx, int cz, World world);
-
-    /**
-     * Unload a given Chunk's Chunklet data
-     *
-     * @param cx Chunk X coordinate that is to be unloaded
-     * @param cz Chunk Z coordinate that is to be unloaded
-     * @param world World that the Chunk is in
-     */
-    void unloadChunk(int cx, int cz, World world);
-
-    /**
-     * Informs the ChunkletManager a chunk is loaded
-     *
-     * @param cx Chunk X coordinate that is loaded
-     * @param cz Chunk Z coordinate that is loaded
-     * @param world World that the chunk was loaded in
-     */
-    void chunkLoaded(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);
-
-    /**
-     * Load all ChunkletStores from all loaded chunks from this world into memory
-     *
-     * @param world World to load
-     */
-    void loadWorld(World world);
-
-    /**
-     * Save all ChunkletStores
-     */
-    void saveAll();
-
-    /**
-     * Unload all ChunkletStores after saving them
-     */
-    void unloadAll();
-
-    /**
-     * 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);
-
-    /**
-     * 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 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);
-
-    /**
-     * Delete any ChunkletStores that are empty
-     */
-    void cleanUp();
-}

+ 0 - 15
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java

@@ -1,15 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import com.gmail.nossr50.config.HiddenConfig;
-
-public class ChunkletManagerFactory {
-    public static ChunkletManager getChunkletManager() {
-        HiddenConfig hConfig = HiddenConfig.getInstance();
-
-        if (hConfig.getChunkletsEnabled()) {
-            return new HashChunkletManager();
-        }
-
-        return new NullChunkletManager();
-    }
-}

+ 0 - 48
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java

@@ -1,48 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import java.io.Serializable;
-
-/**
- * A ChunkletStore should be responsible for a 16x16x64 area of data
- */
-public interface ChunkletStore extends Serializable {
-    /**
-     * Checks the value at the given coordinates
-     *
-     * @param x x coordinate in current chunklet
-     * @param y y coordinate in current chunklet
-     * @param z z coordinate in current chunklet
-     * @return true if the value is true at the given coordinates, false if otherwise
-     */
-    boolean isTrue(int x, int y, int z);
-
-    /**
-     * Set the value to true at the given coordinates
-     *
-     * @param x x coordinate in current chunklet
-     * @param y y coordinate in current chunklet
-     * @param z z coordinate in current chunklet
-     */
-    void setTrue(int x, int y, int z);
-
-    /**
-     * Set the value to false at the given coordinates
-     *
-     * @param x x coordinate in current chunklet
-     * @param y y coordinate in current chunklet
-     * @param z z coordinate in current chunklet
-     */
-    void setFalse(int x, int y, int z);
-
-    /**
-     * @return true if all values in the chunklet are false, false if otherwise
-     */
-    boolean isEmpty();
-
-    /**
-     * Set all values in this ChunkletStore to the values from another provided ChunkletStore
-     *
-     * @param otherStore Another ChunkletStore that this one should copy all data from
-     */
-    void copyFrom(ChunkletStore otherStore);
-}

+ 0 - 8
src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java

@@ -1,8 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-public class ChunkletStoreFactory {
-    protected static ChunkletStore getChunkletStore() {
-        // TODO: Add in loading from config what type of store we want.
-        return new PrimitiveExChunkletStore();
-    }
-}

+ 354 - 0
src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java

@@ -0,0 +1,354 @@
+package com.gmail.nossr50.util.blockmeta;
+
+import com.gmail.nossr50.mcMMO;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+
+import java.io.*;
+import java.util.*;
+
+public class HashChunkManager implements ChunkManager {
+    private final HashMap<CoordinateKey, McMMOSimpleRegionFile> regionMap = new HashMap<>(); // Tracks active regions
+    private final HashMap<CoordinateKey, HashSet<CoordinateKey>> chunkUsageMap = new HashMap<>(); // Tracks active chunks by region
+    private final HashMap<CoordinateKey, ChunkStore> chunkMap = new HashMap<>(); // Tracks active chunks
+
+    @Override
+    public synchronized void closeAll() {
+        // Save all dirty chunkstores
+        for (ChunkStore chunkStore : chunkMap.values())
+        {
+            if (!chunkStore.isDirty())
+                continue;
+            writeChunkStore(Bukkit.getWorld(chunkStore.getWorldId()), chunkStore);
+        }
+        // Clear in memory chunks
+        chunkMap.clear();
+        chunkUsageMap.clear();
+        // Close all region files
+        for (McMMOSimpleRegionFile rf : regionMap.values())
+            rf.close();
+        regionMap.clear();
+    }
+
+    private synchronized ChunkStore readChunkStore(World world, int cx, int cz) throws IOException {
+        McMMOSimpleRegionFile rf = getSimpleRegionFile(world, cx, cz, false);
+        if (rf == null)
+            return null; // If there is no region file, there can't be a chunk
+        try (DataInputStream in = rf.getInputStream(cx, cz)) { // Get input stream for chunk
+            if (in == null)
+                return null; // No chunk
+            return BitSetChunkStore.Serialization.readChunkStore(in); // Read in the chunkstore
+        }
+    }
+
+    private synchronized void writeChunkStore(World world, ChunkStore data) {
+        if (!data.isDirty())
+            return; // Don't save unchanged data
+        try {
+            McMMOSimpleRegionFile rf = getSimpleRegionFile(world, data.getChunkX(), data.getChunkZ(), true);
+            try (DataOutputStream out = rf.getOutputStream(data.getChunkX(), data.getChunkZ())) {
+                BitSetChunkStore.Serialization.writeChunkStore(out, data);
+            }
+            data.setDirty(false);
+        }
+        catch (IOException e) {
+            throw new RuntimeException("Unable to write chunk meta data for " + data.getChunkX() + ", " + data.getChunkZ(), e);
+        }
+    }
+
+    private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int cx, int cz, boolean createIfAbsent) {
+        CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
+
+        return regionMap.computeIfAbsent(regionKey, k -> {
+            File worldRegionsDirectory = new File(world.getWorldFolder(), "mcmmo_regions");
+            if (!createIfAbsent && !worldRegionsDirectory.isDirectory())
+                return null; // Don't create the directory on read-only operations
+            worldRegionsDirectory.mkdirs(); // Ensure directory exists
+            File regionFile = new File(worldRegionsDirectory, "mcmmo_" + regionKey.x + "_" + regionKey.z + "_.mcm");
+            if (!createIfAbsent && !regionFile.exists())
+                return null; // Don't create the file on read-only operations
+            return new McMMOSimpleRegionFile(regionFile, regionKey.x, regionKey.z);
+        });
+    }
+
+    private ChunkStore loadChunk(int cx, int cz, World world) {
+        try {
+            return readChunkStore(world, cx, cz);
+        }
+        catch (Exception ignored) {}
+
+        return null;
+    }
+
+    private void unloadChunk(int cx, int cz, World world) {
+        CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
+        ChunkStore chunkStore = chunkMap.remove(chunkKey); // Remove from chunk map
+        if (chunkStore == null)
+            return;
+
+        if (chunkStore.isDirty())
+            writeChunkStore(world, chunkStore);
+
+        CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
+        HashSet<CoordinateKey> chunkKeys = chunkUsageMap.get(regionKey);
+        chunkKeys.remove(chunkKey); // remove from region file in-use set
+        if (chunkKeys.isEmpty()) // If it was last chunk in region, close the region file and remove it from memory
+        {
+            chunkUsageMap.remove(regionKey);
+            regionMap.remove(regionKey).close();
+        }
+    }
+
+    @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;
+
+        unloadChunk(cx, cz, world);
+    }
+
+    @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;
+
+        UUID wID = world.getUID();
+
+        // Save and remove all the chunks
+        List<CoordinateKey> chunkKeys = new ArrayList<>(chunkMap.keySet());
+        for (CoordinateKey chunkKey : chunkKeys) {
+            if (!wID.equals(chunkKey.worldID))
+                continue;
+            ChunkStore chunkStore = chunkMap.remove(chunkKey);
+            if (!chunkStore.isDirty())
+                continue;
+            try {
+                writeChunkStore(world, chunkStore);
+            }
+            catch (Exception ignore) { }
+        }
+        // Clear all the region files
+        List<CoordinateKey> regionKeys = new ArrayList<>(regionMap.keySet());
+        for (CoordinateKey regionKey : regionKeys) {
+            if (!wID.equals(regionKey.worldID))
+                continue;
+            regionMap.remove(regionKey).close();
+            chunkUsageMap.remove(regionKey);
+        }
+    }
+
+    @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;
+
+        CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
+
+        // Get chunk, load from file if necessary
+        // Get/Load/Create chunkstore
+        ChunkStore check = chunkMap.computeIfAbsent(chunkKey, k -> {
+            // Load from file
+            ChunkStore loaded = loadChunk(chunkKey.x, chunkKey.z, world);
+            if (loaded == null)
+                return null;
+            // Mark chunk in-use for region tracking
+            chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
+            return loaded;
+        });
+
+        // No chunk, return false
+        if (check == null)
+            return false;
+
+        int ix = Math.abs(x) % 16;
+        int iz = Math.abs(z) % 16;
+
+        return check.isTrue(ix, y, iz);
+    }
+
+    @Override
+    public synchronized boolean isTrue(Block block) {
+        if (block == null)
+            return false;
+
+        return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
+    }
+
+    @Override
+    public synchronized boolean isTrue(BlockState blockState) {
+        if (blockState == null)
+            return false;
+
+        return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+    }
+
+    @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());
+    }
+
+    @Override
+    public synchronized void setTrue(BlockState blockState) {
+        if (blockState == null)
+            return;
+
+        setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
+    }
+
+    @Override
+    public synchronized void setFalse(int x, int y, int z, World world) {
+        set(x, y, z, world, false);
+    }
+
+    @Override
+    public synchronized void setFalse(Block block) {
+        if (block == null)
+            return;
+
+        setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
+    }
+
+    @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;
+
+        CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
+
+        // Get/Load/Create chunkstore
+        ChunkStore cStore = chunkMap.computeIfAbsent(chunkKey, k -> {
+            // Load from file
+            ChunkStore loaded = loadChunk(chunkKey.x, chunkKey.z, world);
+            if (loaded != null)
+            {
+                chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
+                return loaded;
+            }
+            // If setting to false, no need to create an empty chunkstore
+            if (!value)
+                return null;
+            // Mark chunk in-use for region tracking
+            chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
+            // Create a new chunkstore
+            return new BitSetChunkStore(world, chunkKey.x, chunkKey.z);
+        });
+
+        // Indicates setting false on empty chunkstore
+        if (cStore == null)
+            return;
+
+        // Get block offset (offset from chunk corner)
+        int ix = Math.abs(x) % 16;
+        int iz = Math.abs(z) % 16;
+
+        // Set chunk store value
+        cStore.set(ix, y, iz, value);
+    }
+
+    private CoordinateKey blockCoordinateToChunkKey(UUID worldUid, int x, int y, int z) {
+        return toChunkKey(worldUid, x >> 4, z >> 4);
+    }
+
+    private CoordinateKey toChunkKey(UUID worldUid, int cx, int cz){
+        return new CoordinateKey(worldUid, cx, cz);
+    }
+
+    private CoordinateKey toRegionKey(UUID worldUid, int cx, int cz) {
+        // Compute region index (32x32 chunk regions)
+        int rx = cx >> 5;
+        int rz = cz >> 5;
+        return new CoordinateKey(worldUid, rx, rz);
+    }
+
+    private static final class CoordinateKey {
+        public final UUID worldID;
+        public final int x;
+        public final int z;
+
+        private CoordinateKey(UUID worldID, int x, int z) {
+            this.worldID = worldID;
+            this.x = x;
+            this.z = z;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            CoordinateKey coordinateKey = (CoordinateKey) o;
+            return x == coordinateKey.x &&
+                    z == coordinateKey.z &&
+                    worldID.equals(coordinateKey.worldID);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(worldID, x, z);
+        }
+    }
+
+    @Override
+    public synchronized void cleanUp() {}
+}

+ 0 - 410
src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java

@@ -1,410 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.World;
-import org.bukkit.block.Block;
-
-import java.io.*;
-import java.util.HashMap;
-
-public class HashChunkletManager implements ChunkletManager {
-    public HashMap<String, ChunkletStore> store = new HashMap<>();
-
-    @Override
-    public void loadChunklet(int cx, int cy, int cz, World world) {
-        File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
-        File cxDir = new File(dataDir, "" + cx);
-        if (!cxDir.exists()) {
-            return;
-        }
-        File czDir = new File(cxDir, "" + cz);
-        if (!czDir.exists()) {
-            return;
-        }
-        File yFile = new File(czDir, "" + cy);
-        if (!yFile.exists()) {
-            return;
-        }
-
-        ChunkletStore in = deserializeChunkletStore(yFile);
-        if (in != null) {
-            store.put(world.getName() + "," + cx + "," + cz + "," + cy, in);
-        }
-    }
-
-    @Override
-    public void unloadChunklet(int cx, int cy, int cz, World world) {
-        File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
-        if (store.containsKey(world.getName() + "," + cx + "," + cz + "," + cy)) {
-            File cxDir = new File(dataDir, "" + cx);
-            if (!cxDir.exists()) {
-                cxDir.mkdir();
-            }
-            File czDir = new File(cxDir, "" + cz);
-            if (!czDir.exists()) {
-                czDir.mkdir();
-            }
-            File yFile = new File(czDir, "" + cy);
-
-            ChunkletStore out = store.get(world.getName() + "," + cx + "," + cz + "," + cy);
-            serializeChunkletStore(out, yFile);
-            store.remove(world.getName() + "," + cx + "," + cz + "," + cy);
-        }
-    }
-
-    @Override
-    public void loadChunk(int cx, int cz, World world) {
-        File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
-        File cxDir = new File(dataDir, "" + cx);
-        if (!cxDir.exists()) {
-            return;
-        }
-        File czDir = new File(cxDir, "" + cz);
-        if (!czDir.exists()) {
-            return;
-        }
-
-        for (int y = 0; y < 4; y++) {
-            File yFile = new File(czDir, "" + y);
-            if (!yFile.exists()) {
-                continue;
-            }
-
-            ChunkletStore in = deserializeChunkletStore(yFile);
-            if (in != null) {
-                store.put(world.getName() + "," + cx + "," + cz + "," + y, in);
-            }
-        }
-    }
-
-    @Override
-    public void unloadChunk(int cx, int cz, World world) {
-        File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
-
-        for (int y = 0; y < 4; y++) {
-            if (store.containsKey(world.getName() + "," + cx + "," + cz + "," + y)) {
-                File cxDir = new File(dataDir, "" + cx);
-                if (!cxDir.exists()) {
-                    cxDir.mkdir();
-                }
-                File czDir = new File(cxDir, "" + cz);
-                if (!czDir.exists()) {
-                    czDir.mkdir();
-                }
-                File yFile = new File(czDir, "" + y);
-
-                ChunkletStore out = store.get(world.getName() + "," + cx + "," + cz + "," + y);
-                serializeChunkletStore(out, yFile);
-                store.remove(world.getName() + "," + cx + "," + cz + "," + y);
-            }
-        }
-    }
-
-    @Override
-    public void chunkLoaded(int cx, int cz, World world) {
-        //loadChunk(cx, cz, world);
-    }
-
-    @Override
-    public void chunkUnloaded(int cx, int cz, World world) {
-        unloadChunk(cx, cx, world);
-    }
-
-    @Override
-    public void saveWorld(World world) {
-        String worldName = world.getName();
-        File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
-        if (!dataDir.exists()) {
-            dataDir.mkdirs();
-        }
-
-        for (String key : store.keySet()) {
-            String[] info = key.split(",");
-            if (worldName.equals(info[0])) {
-                File cxDir = new File(dataDir, "" + info[1]);
-                if (!cxDir.exists()) {
-                    cxDir.mkdir();
-                }
-                File czDir = new File(cxDir, "" + info[2]);
-                if (!czDir.exists()) {
-                    czDir.mkdir();
-                }
-
-                File yFile = new File(czDir, "" + info[3]);
-                serializeChunkletStore(store.get(key), yFile);
-            }
-        }
-    }
-
-    @Override
-    public void unloadWorld(World world) {
-        saveWorld(world);
-
-        String worldName = world.getName();
-
-        for (String key : store.keySet()) {
-            String tempWorldName = key.split(",")[0];
-            if (tempWorldName.equals(worldName)) {
-                store.remove(key);
-                return;
-            }
-        }
-    }
-
-    @Override
-    public void loadWorld(World world) {
-        //for (Chunk chunk : world.getLoadedChunks()) {
-        //  this.chunkLoaded(chunk.getX(), chunk.getZ(), world);
-        //}
-    }
-
-    @Override
-    public void saveAll() {
-        for (World world : mcMMO.p.getServer().getWorlds()) {
-            saveWorld(world);
-        }
-    }
-
-    @Override
-    public void unloadAll() {
-        saveAll();
-        for (World world : mcMMO.p.getServer().getWorlds()) {
-            unloadWorld(world);
-        }
-    }
-
-    @Override
-    public boolean isTrue(int x, int y, int z, World world) {
-        int cx = x >> 4;
-        int cz = z >> 4;
-        int cy = y >> 6;
-
-        String key = world.getName() + "," + cx + "," + cz + "," + cy;
-
-        if (!store.containsKey(key)) {
-            loadChunklet(cx, cy, cz, world);
-        }
-
-        if (!store.containsKey(key)) {
-            return false;
-        }
-
-        ChunkletStore check = store.get(world.getName() + "," + cx + "," + cz + "," + cy);
-        int ix = Math.abs(x) % 16;
-        int iz = Math.abs(z) % 16;
-        int iy = Math.abs(y) % 64;
-
-        return check.isTrue(ix, iy, iz);
-    }
-
-    @Override
-    public boolean isTrue(Block block) {
-        return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
-    }
-
-    @Override
-    public void setTrue(int x, int y, int z, World world) {
-        int cx = x >> 4;
-        int cz = z >> 4;
-        int cy = y >> 6;
-
-        int ix = Math.abs(x) % 16;
-        int iz = Math.abs(z) % 16;
-        int iy = Math.abs(y) % 64;
-
-        String key = world.getName() + "," + cx + "," + cz + "," + cy;
-
-        if (!store.containsKey(key)) {
-            loadChunklet(cx, cy, cz, world);
-        }
-
-        ChunkletStore cStore = store.get(key);
-
-        if (cStore == null) {
-            cStore = ChunkletStoreFactory.getChunkletStore();
-
-            store.put(world.getName() + "," + cx + "," + cz + "," + cy, cStore);
-        }
-
-        cStore.setTrue(ix, iy, iz);
-    }
-
-    @Override
-    public void setTrue(Block block) {
-        setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
-    }
-
-    @Override
-    public void setFalse(int x, int y, int z, World world) {
-        int cx = x >> 4;
-        int cz = z >> 4;
-        int cy = y >> 6;
-
-        int ix = Math.abs(x) % 16;
-        int iz = Math.abs(z) % 16;
-        int iy = Math.abs(y) % 64;
-
-        String key = world.getName() + "," + cx + "," + cz + "," + cy;
-
-        if (!store.containsKey(key)) {
-            loadChunklet(cx, cy, cz, world);
-        }
-
-        ChunkletStore cStore = store.get(key);
-
-        if (cStore == null) {
-            return; // No need to make a store for something we will be setting to false
-        }
-
-        cStore.setFalse(ix, iy, iz);
-    }
-
-    @Override
-    public void setFalse(Block block) {
-        setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
-    }
-
-    @Override
-    public void cleanUp() {
-        for (String key : store.keySet()) {
-            if (store.get(key).isEmpty()) {
-                String[] info = key.split(",");
-                File dataDir = new File(mcMMO.p.getServer().getWorld(info[0]).getWorldFolder(), "mcmmo_data");
-
-                File cxDir = new File(dataDir, "" + info[1]);
-                if (!cxDir.exists()) {
-                    continue;
-                }
-                File czDir = new File(cxDir, "" + info[2]);
-                if (!czDir.exists()) {
-                    continue;
-                }
-
-                File yFile = new File(czDir, "" + info[3]);
-                yFile.delete();
-
-                // Delete empty directories
-                if (czDir.list().length == 0) {
-                    czDir.delete();
-                }
-                if (cxDir.list().length == 0) {
-                    cxDir.delete();
-                }
-            }
-        }
-    }
-
-    /**
-     * @param cStore ChunkletStore to save
-     * @param location Where on the disk to put it
-     */
-    private void serializeChunkletStore(ChunkletStore cStore, File location) {
-        FileOutputStream fileOut = null;
-        ObjectOutputStream objOut = null;
-
-        try {
-            if (!location.exists()) {
-                location.createNewFile();
-            }
-            fileOut = new FileOutputStream(location);
-            objOut = new ObjectOutputStream(fileOut);
-            objOut.writeObject(cStore);
-        }
-        catch (IOException ex) {
-            ex.printStackTrace();
-        }
-        finally {
-            if (objOut != null) {
-                try {
-                    objOut.flush();
-                    objOut.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
-            }
-
-            if (fileOut != null) {
-                try {
-                    fileOut.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
-            }
-        }
-    }
-
-    /**
-     * @param location Where on the disk to read from
-     * @return ChunkletStore from the specified location
-     */
-    private ChunkletStore deserializeChunkletStore(File location) {
-        ChunkletStore storeIn = null;
-        FileInputStream fileIn = null;
-        ObjectInputStream objIn = null;
-
-        try {
-            fileIn = new FileInputStream(location);
-            objIn = new ObjectInputStream(new BufferedInputStream(fileIn));
-            storeIn = (ChunkletStore) objIn.readObject();
-        }
-        catch (IOException ex) {
-            if (ex instanceof EOFException) {
-                // EOF should only happen on Chunklets that somehow have been corrupted.
-                //mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " could not be read due to an EOFException, data in this area will be lost.");
-                return ChunkletStoreFactory.getChunkletStore();
-            }
-            else if (ex instanceof StreamCorruptedException) {
-                // StreamCorrupted happens when the Chunklet is no good.
-                //mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " is corrupted, data in this area will be lost.");
-                return ChunkletStoreFactory.getChunkletStore();
-            }
-            else if (ex instanceof UTFDataFormatException) {
-                // UTF happens when the Chunklet cannot be read or is corrupted
-                //mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " could not be read due to an UTFDataFormatException, data in this area will be lost.");
-                return ChunkletStoreFactory.getChunkletStore();
-            }
-
-            ex.printStackTrace();
-        }
-        catch (ClassNotFoundException ex) {
-            ex.printStackTrace();
-        }
-        finally {
-            if (objIn != null) {
-                try {
-                    objIn.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
-            }
-
-            if (fileIn != null) {
-                try {
-                    fileIn.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
-            }
-        }
-
-        // TODO: Make this less messy, as it is, it's kinda... depressing to do it like this.
-        // Might also make a mess when we move to stacks, but at that point I think I will write a new Manager...
-        // IMPORTANT! If ChunkletStoreFactory is going to be returning something other than PrimitiveEx we need to remove this, as it will be breaking time for old maps
-
-        /*
-        if (!(storeIn instanceof PrimitiveExChunkletStore)) {
-            ChunkletStore tempStore = ChunkletStoreFactory.getChunkletStore();
-            if (storeIn != null) {
-                tempStore.copyFrom(storeIn);
-            }
-            storeIn = tempStore;
-        }
-         */
-
-        return storeIn;
-    }
-}

+ 257 - 0
src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java

@@ -0,0 +1,257 @@
+/*
+ * This file is part of SpoutPlugin.
+ *
+ * Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
+ * SpoutPlugin is licensed under the GNU Lesser General Public License.
+ *
+ * SpoutPlugin is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * SpoutPlugin is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.gmail.nossr50.util.blockmeta;
+
+import java.io.*;
+import java.util.BitSet;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * File format:
+ * bytes 0-4096 contain 1024 integer values representing the segment index of each chunk
+ * bytes 4096-8192 contain 1024 integer values representing the byte length of each chunk
+ * bytes 8192-8196 is the integer value of the segment exponent
+ * bytes 8196-12288 are reserved for future use
+ * bytes 12288+ contain the data segments, by default 1024 byte segments.
+ * Chunk data is compressed and stored in 1 or more segments as needed.
+ */
+public class McMMOSimpleRegionFile {
+    private static final int DEFAULT_SEGMENT_EXPONENT = 10; // TODO, analyze real world usage and determine if a smaller segment(512) is worth it or not. (need to know average chunkstore bytesize)
+    private static final int DEFAULT_SEGMENT_SIZE = (int)Math.pow(2, DEFAULT_SEGMENT_EXPONENT); // 1024
+    private static final int RESERVED_HEADER_BYTES = 12288; // This needs to be divisible by segment size
+    private static final int NUM_CHUNKS = 1024; // 32x32
+    private static final int SEEK_CHUNK_SEGMENT_INDICES = 0;
+    private static final int SEEK_CHUNK_BYTE_LENGTHS = 4096;
+    private static final int SEEK_FILE_INFO = 8192;
+    // Chunk info
+    private final int[] chunkSegmentIndex = new int[NUM_CHUNKS];
+    private final int[] chunkNumBytes = new int[NUM_CHUNKS];
+    private final int[] chunkNumSegments = new int[NUM_CHUNKS];
+
+    // Segments
+    private final BitSet segments = new BitSet(); // Used to denote which segments are in use or not
+
+    // Segment size/mask
+    private final int segmentExponent;
+    private final int segmentMask;
+
+    // File location
+    private final File parent;
+    // File access
+    private final RandomAccessFile file;
+
+    // Region index
+    private final int rx;
+    private final int rz;
+
+    public McMMOSimpleRegionFile(File f, int rx, int rz) {
+        this.rx = rx;
+        this.rz = rz;
+        this.parent = f;
+
+        try {
+            this.file = new RandomAccessFile(parent, "rw");
+
+            // New file, write out header bytes
+            if (file.length() < RESERVED_HEADER_BYTES) {
+                file.write(new byte[RESERVED_HEADER_BYTES]);
+                file.seek(SEEK_FILE_INFO);
+                file.writeInt(DEFAULT_SEGMENT_EXPONENT);
+            }
+
+            file.seek(SEEK_FILE_INFO);
+            this.segmentExponent = file.readInt();
+            this.segmentMask = (1 << segmentExponent) - 1;
+
+            // Mark reserved segments reserved
+            int reservedSegments = this.bytesToSegments(RESERVED_HEADER_BYTES);
+            segments.set(0, reservedSegments, true);
+
+            // Read chunk header data
+            file.seek(SEEK_CHUNK_SEGMENT_INDICES);
+            for (int i = 0; i < NUM_CHUNKS; i++)
+                chunkSegmentIndex[i] = file.readInt();
+
+            file.seek(SEEK_CHUNK_BYTE_LENGTHS);
+            for (int i = 0; i < NUM_CHUNKS; i++) {
+                chunkNumBytes[i] = file.readInt();
+                chunkNumSegments[i] = bytesToSegments(chunkNumBytes[i]);
+                markChunkSegments(i, true);
+            }
+
+            fixFileLength();
+        }
+        catch (IOException fnfe) {
+            throw new RuntimeException(fnfe);
+        }
+    }
+
+    public synchronized DataOutputStream getOutputStream(int x, int z) {
+        int index = getChunkIndex(x, z); // Get chunk index
+        return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
+    }
+
+    private static class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
+        final McMMOSimpleRegionFile rf;
+        final int index;
+
+        McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
+            super(DEFAULT_SEGMENT_SIZE);
+            this.rf = rf;
+            this.index = index;
+        }
+
+        @Override
+        public void close() throws IOException {
+            rf.write(index, buf, count);
+        }
+    }
+
+    private synchronized void write(int index, byte[] buffer, int size) throws IOException {
+        int oldSegmentIndex = chunkSegmentIndex[index]; // Get current segment index
+        markChunkSegments(index, false); // Clear our old segments
+        int newSegmentIndex = findContiguousSegments(oldSegmentIndex, size); // Find contiguous segments to save to
+        file.seek(newSegmentIndex << segmentExponent); // Seek to file location
+        file.write(buffer, 0, size); // Write data
+        // update in memory info
+        chunkSegmentIndex[index] = newSegmentIndex;
+        chunkNumBytes[index] = size;
+        chunkNumSegments[index] = bytesToSegments(size);
+        // Mark segments in use
+        markChunkSegments(index, true);
+        // Update header info
+        file.seek(SEEK_CHUNK_SEGMENT_INDICES + (4 * index));
+        file.writeInt(chunkSegmentIndex[index]);
+        file.seek(SEEK_CHUNK_BYTE_LENGTHS + (4 * index));
+        file.writeInt(chunkNumBytes[index]);
+    }
+
+    public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
+        int index = getChunkIndex(x, z); // Get chunk index
+        int byteLength = chunkNumBytes[index]; // Get byte length of data
+
+        // No bytes
+        if (byteLength == 0)
+            return null;
+
+        byte[] data = new byte[byteLength];
+
+        file.seek(chunkSegmentIndex[index] << segmentExponent); // Seek to file location
+        file.readFully(data); // Read in the data
+        return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
+    }
+
+    public synchronized void close() {
+        try {
+            file.close();
+            segments.clear();
+        }
+        catch (IOException ioe) {
+            throw new RuntimeException("Unable to close file", ioe);
+        }
+    }
+
+    private synchronized void markChunkSegments(int index, boolean inUse) {
+        // No bytes used
+        if (chunkNumBytes[index] == 0)
+            return;
+
+        int start = chunkSegmentIndex[index];
+        int end = start + chunkNumSegments[index];
+
+        // If we are writing, assert we don't write over any in-use segments
+        if (inUse)
+        {
+            int nextSetBit = segments.nextSetBit(start);
+            if (nextSetBit != -1 && nextSetBit < end)
+                throw new IllegalStateException("Attempting to overwrite an in-use segment");
+        }
+
+        segments.set(start, end, inUse);
+    }
+
+    private synchronized void fixFileLength() throws IOException {
+        int fileLength = (int)file.length();
+        int extend = -fileLength & segmentMask; // how many bytes do we need to be divisible by segment size
+
+        // Go to end of file
+        file.seek(fileLength);
+        // Append bytes
+        file.write(new byte[extend], 0, extend);
+    }
+
+    private synchronized int findContiguousSegments(int hint, int size) {
+        if (size == 0)
+            return 0; // Zero byte data will not claim any chunks anyways
+
+        int segments = bytesToSegments(size); // Number of segments we need
+
+        // Check the hinted location (previous location of chunk) most of the time we can fit where we were.
+        boolean oldFree = true;
+        for (int i = hint; i < this.segments.size() && i < hint + segments; i++) {
+            if (this.segments.get(i)) {
+                oldFree = false;
+                break;
+            }
+        }
+
+        // We fit!
+        if (oldFree)
+            return hint;
+
+        // Find somewhere to put us
+        int start = 0;
+        int current = 0;
+
+        while (current < this.segments.size()) {
+            boolean segmentInUse = this.segments.get(current); // check if segment is in use
+            current++; // Move up a segment
+
+            // Move up start if the segment was in use
+            if (segmentInUse)
+                start = current;
+
+            // If we have enough segments now, return
+            if (current - start >= segments)
+                return start;
+        }
+
+        // Return the end of the segments (will expand to fit them)
+        return start;
+    }
+
+    private synchronized int bytesToSegments(int bytes) {
+        if (bytes <= 0)
+            return 1;
+
+        return ((bytes - 1) >> segmentExponent) + 1; // ((bytes - 1) / segmentSize) + 1
+    }
+
+    private synchronized int getChunkIndex(int x, int z) {
+        if (rx != (x >> 5) || rz != (z >> 5))
+            throw new IndexOutOfBoundsException();
+
+        x = x & 0x1F; // 5 bits (mod 32)
+        z = z & 0x1F; // 5 bits (mod 32)
+
+        return (x << 5) + z; // x in the upper 5 bits, z in the lower 5 bits
+    }
+}

+ 1 - 41
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java → src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java

@@ -1,51 +1,17 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
+package com.gmail.nossr50.util.blockmeta;
 
 import org.bukkit.World;
 import org.bukkit.block.Block;
 import org.bukkit.block.BlockState;
-import org.bukkit.entity.Entity;
-
-import java.io.IOException;
 
 public class NullChunkManager implements ChunkManager {
 
     @Override
     public void closeAll() {}
 
-    @Override
-    public ChunkStore readChunkStore(World world, int x, int z) throws IOException {
-        return null;
-    }
-
-    @Override
-    public void writeChunkStore(World world, int x, int z, ChunkStore data) {}
-
-    @Override
-    public void closeChunkStore(World world, int x, int z) {}
-
-    @Override
-    public void loadChunklet(int cx, int cy, int cz, World world) {}
-
-    @Override
-    public void unloadChunklet(int cx, int cy, int cz, World world) {}
-
-    @Override
-    public void loadChunk(int cx, int cz, World world, Entity[] entities) {}
-
-    @Override
-    public void unloadChunk(int cx, int cz, World world) {}
-
     @Override
     public void saveChunk(int cx, int cz, World world) {}
 
-    @Override
-    public boolean isChunkLoaded(int cx, int cz, World world) {
-        return true;
-    }
-
-    @Override
-    public void chunkLoaded(int cx, int cz, World world) {}
-
     @Override
     public void chunkUnloaded(int cx, int cz, World world) {}
 
@@ -55,15 +21,9 @@ public class NullChunkManager implements ChunkManager {
     @Override
     public void unloadWorld(World world) {}
 
-    @Override
-    public void loadWorld(World world) {}
-
     @Override
     public void saveAll() {}
 
-    @Override
-    public void unloadAll() {}
-
     @Override
     public boolean isTrue(int x, int y, int z, World world) {
         return false;

+ 0 - 85
src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java

@@ -1,85 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import org.bukkit.World;
-import org.bukkit.block.Block;
-
-/**
- * A ChunkletManager implementation that does nothing and returns false for all checks.
- *
- * Useful for turning off Chunklets without actually doing much work
- */
-public class NullChunkletManager implements ChunkletManager {
-    @Override
-    public void loadChunklet(int cx, int cy, int cz, World world) {
-    }
-
-    @Override
-    public void unloadChunklet(int cx, int cy, int cz, World world) {
-    }
-
-    @Override
-    public void loadChunk(int cx, int cz, World world) {
-    }
-
-    @Override
-    public void unloadChunk(int cx, int cz, World world) {
-    }
-
-    @Override
-    public void chunkLoaded(int cx, int cz, World world) {
-    }
-
-    @Override
-    public void chunkUnloaded(int cx, int cz, World world) {
-    }
-
-    @Override
-    public void saveWorld(World world) {
-    }
-
-    @Override
-    public void unloadWorld(World world) {
-    }
-
-    @Override
-    public void loadWorld(World world) {
-    }
-
-    @Override
-    public void saveAll() {
-    }
-
-    @Override
-    public void unloadAll() {
-    }
-
-    @Override
-    public boolean isTrue(int x, int y, int z, World world) {
-        return false;
-    }
-
-    @Override
-    public boolean isTrue(Block block) {
-        return false;
-    }
-
-    @Override
-    public void setTrue(int x, int y, int z, World world) {
-    }
-
-    @Override
-    public void setTrue(Block block) {
-    }
-
-    @Override
-    public void setFalse(int x, int y, int z, World world) {
-    }
-
-    @Override
-    public void setFalse(Block block) {
-    }
-
-    @Override
-    public void cleanUp() {
-    }
-}

+ 0 - 48
src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java

@@ -1,48 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-public class PrimitiveChunkletStore implements ChunkletStore {
-    private static final long serialVersionUID = -3453078050608607478L;
-
-    /** X, Z, Y */
-    public boolean[][][] store = new boolean[16][16][64];
-
-    @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) {
-        store[x][z][y] = true;
-    }
-
-    @Override
-    public void setFalse(int x, int y, int z) {
-        store[x][z][y] = false;
-    }
-
-    @Override
-    public boolean isEmpty() {
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < 64; y++) {
-                    if (store[x][z][y]) {
-                        return false;
-                    }
-                }
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public void copyFrom(ChunkletStore otherStore) {
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < 64; y++) {
-                    store[x][z][y] = otherStore.isTrue(x, y, z);
-                }
-            }
-        }
-    }
-}

+ 0 - 180
src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java

@@ -1,180 +0,0 @@
-package com.gmail.nossr50.util.blockmeta;
-
-import java.io.Externalizable;
-import java.io.IOException;
-import java.io.ObjectInput;
-import java.io.ObjectOutput;
-
-public class PrimitiveExChunkletStore implements ChunkletStore, Externalizable {
-    private static final long serialVersionUID = 8603603827094383873L;
-
-    /** X, Z, Y */
-    public boolean[][][] store = new boolean[16][16][64];
-
-    @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) {
-        store[x][z][y] = true;
-    }
-
-    @Override
-    public void setFalse(int x, int y, int z) {
-        store[x][z][y] = false;
-    }
-
-    @Override
-    public boolean isEmpty() {
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < 64; y++) {
-                    if (store[x][z][y]) {
-                        return false;
-                    }
-                }
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public void copyFrom(ChunkletStore otherStore) {
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < 64; y++) {
-                    store[x][z][y] = otherStore.isTrue(x, y, z);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void writeExternal(ObjectOutput out) throws IOException {
-        byte[] buffer = new byte[2304]; // 2304 is 16*16*9
-        int bufferIndex = 0;
-
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < 64; y++) {
-                    if (store[x][z][y]) {
-                        byte[] temp = constructColumn(x, z);
-
-                        for (int i = 0; i < 9; i++) {
-                            buffer[bufferIndex] = temp[i];
-                            bufferIndex++;
-                        }
-
-                        break;
-                    }
-                }
-            }
-        }
-
-        out.write(buffer, 0, bufferIndex);
-        out.flush();
-    }
-
-    // For this we assume that store has been initialized to be all false by now
-    @Override
-    public void readExternal(ObjectInput in) throws IOException {
-        byte[] temp = new byte[9];
-
-        // Could probably reorganize this loop to print nasty things if it does not equal 9 or -1
-        while (in.read(temp, 0, 9) == 9) {
-            int x = addressByteX(temp[0]);
-            int z = addressByteZ(temp[0]);
-            boolean[] yColumn = new boolean[64];
-
-            for (int i = 0; i < 8; i++) {
-                for (int j = 0; j < 8; j++) {
-                    yColumn[j + (i * 8)] = (temp[i + 1] & (1 << j)) != 0;
-                }
-            }
-
-            store[x][z] = yColumn;
-        }
-    }
-
-    /*
-     * The column: An array of 9 bytes which represent all y values for a given (x,z) Chunklet-coordinate
-     *
-     * The first byte is an address byte, this provides the x and z values.
-     * The next 8 bytes are all y values from 0 to 63, with each byte containing 8 bits of true/false data
-     *
-     * Each of these 8 bytes address to a y value from right to left
-     *
-     * Examples:
-     * 00000001 represents that the lowest y value in this byte is true, all others are off
-     * 10000000 represents that the highest y value in this byte is true, all others are off
-     * 10000001 represents that the lowest and highest y values in this byte are true, all others are off
-     *
-     * Full columns:
-     * See comment on Address byte for information on how to use that byte
-     *
-     * Example:
-     * ADDRESS_BYTE 10000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000
-     *  - x, z from ADDRESS_BYTE
-     *  - The next byte contains data from 0 to 7
-     *    - 1 is set in the highest bit position, this is 7 in y coordinate
-     *  - The next byte contains data from 8 to 15
-     *    - 1 is set in the lowest bit position, this is 8 in the y coordinate
-     *  Therefore, for this column: There are true values at (x, 7, z) and (x, 8, z)
-     */
-    private byte[] constructColumn(int x, int z) {
-        byte[] column = new byte[9];
-        int index = 1;
-
-        column[0] = makeAddressByte(x, z);
-
-        for (int i = 0; i < 8; i++) {
-            byte yCompressed = 0x0;
-            int subColumnIndex = 8 * i;
-            int subColumnEnd = subColumnIndex + 8;
-
-            for (int y = subColumnIndex; y < subColumnEnd; y++) {
-                if (store[x][z][y]) {
-                    yCompressed |= 1 << (y % 8);
-                }
-            }
-
-            column[index] = yCompressed;
-            index++;
-        }
-
-        return column;
-    }
-
-    /*
-     * The address byte: A single byte which contains x and z values which correspond to the x and z Chunklet-coordinates
-     *
-     * In Chunklet-coordinates, the only valid values are 0-15, so we can fit both into a single byte.
-     *
-     * The top 4 bits of the address byte are for the x value
-     * The bottom 4 bits of the address byte are for the z value
-     *
-     * Examples:
-     * An address byte with a value 00000001 would be split like so:
-     *  - x = 0000 = 0
-     *  - z = 0001 = 1
-     *  => Chunklet coordinates (0, 1)
-     *
-     * 01011111
-     *  - x = 0101 = 5
-     *  - z = 1111 = 15
-     *  => Chunklet coordinates (5, 15)
-     */
-    protected static byte makeAddressByte(int x, int z) {
-        return (byte) ((x << 4) + z);
-    }
-
-    protected static int addressByteX(byte address) {
-        return (address & 0xF0) >>> 4;
-    }
-
-    protected static int addressByteZ(byte address) {
-        return address & 0x0F;
-    }
-}

+ 0 - 10
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java

@@ -1,10 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import org.bukkit.World;
-
-public class ChunkStoreFactory {
-    protected static ChunkStore getChunkStore(World world, int x, int z) {
-        // TODO: Add in loading from config what type of store we want.
-        return new PrimitiveChunkStore(world, x, z);
-    }
-}

+ 0 - 447
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java

@@ -1,447 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.blockmeta.conversion.BlockStoreConversionZDirectory;
-import org.bukkit.World;
-import org.bukkit.block.Block;
-import org.bukkit.block.BlockState;
-import org.bukkit.entity.Entity;
-
-import java.io.*;
-import java.util.*;
-
-public class HashChunkManager implements ChunkManager {
-    private final HashMap<UUID, HashMap<Long, McMMOSimpleRegionFile>> regionFiles = new HashMap<>();
-    public HashMap<String, ChunkStore> store = new HashMap<>();
-    public ArrayList<BlockStoreConversionZDirectory> converters = new ArrayList<>();
-    private final HashMap<UUID, Boolean> oldData = new HashMap<>();
-
-    @Override
-    public synchronized void closeAll() {
-        for (UUID uid : regionFiles.keySet()) {
-            HashMap<Long, McMMOSimpleRegionFile> worldRegions = regionFiles.get(uid);
-            for (Iterator<McMMOSimpleRegionFile> worldRegionIterator = worldRegions.values().iterator(); worldRegionIterator.hasNext(); ) {
-                McMMOSimpleRegionFile rf = worldRegionIterator.next();
-                if (rf != null) {
-                    rf.close();
-                    worldRegionIterator.remove();
-                }
-            }
-        }
-        regionFiles.clear();
-    }
-
-    @Override
-    public synchronized ChunkStore readChunkStore(World world, int x, int z) throws IOException {
-        McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
-        InputStream in = rf.getInputStream(x, z);
-        if (in == null) {
-            return null;
-        }
-        try (ObjectInputStream objectStream = new ObjectInputStream(in)) {
-            Object o = objectStream.readObject();
-            if (o instanceof ChunkStore) {
-                return (ChunkStore) o;
-            }
-
-            throw new RuntimeException("Wrong class type read for chunk meta data for " + x + ", " + z);
-        } catch (IOException | ClassNotFoundException e) {
-            e.printStackTrace();
-            // Assume the format changed
-            return null;
-            //throw new RuntimeException("Unable to process chunk meta data for " + x + ", " + z, e);
-        }
-    }
-
-    @Override
-    public synchronized void writeChunkStore(World world, int x, int z, ChunkStore data) {
-        if (!data.isDirty()) {
-            return;
-        }
-        try {
-            McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
-            ObjectOutputStream objectStream = new ObjectOutputStream(rf.getOutputStream(x, z));
-            objectStream.writeObject(data);
-            objectStream.flush();
-            objectStream.close();
-            data.setDirty(false);
-        }
-        catch (IOException e) {
-            throw new RuntimeException("Unable to write chunk meta data for " + x + ", " + z, e);
-        }
-    }
-
-    @Override
-    public synchronized void closeChunkStore(World world, int x, int z) {
-        McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
-        if (rf != null) {
-            rf.close();
-        }
-    }
-
-    private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int x, int z) {
-        File directory = new File(world.getWorldFolder(), "mcmmo_regions");
-
-        directory.mkdirs();
-
-        UUID key = world.getUID();
-
-        HashMap<Long, McMMOSimpleRegionFile> worldRegions = regionFiles.computeIfAbsent(key, k -> new HashMap<>());
-
-        int rx = x >> 5;
-        int rz = z >> 5;
-
-        long key2 = (((long) rx) << 32) | ((rz) & 0xFFFFFFFFL);
-
-        McMMOSimpleRegionFile regionFile = worldRegions.get(key2);
-
-        if (regionFile == null) {
-            File file = new File(directory, "mcmmo_" + rx + "_" + rz + "_.mcm");
-            regionFile = new McMMOSimpleRegionFile(file, rx, rz);
-            worldRegions.put(key2, regionFile);
-        }
-
-        return regionFile;
-    }
-
-    @Override
-    public synchronized void loadChunklet(int cx, int cy, int cz, World world) {
-        loadChunk(cx, cz, world, null);
-    }
-
-    @Override
-    public synchronized void unloadChunklet(int cx, int cy, int cz, World world) {
-        unloadChunk(cx, cz, world);
-    }
-
-    @Override
-    public synchronized void loadChunk(int cx, int cz, World world, Entity[] entities) {
-        if (world == null || store.containsKey(world.getName() + "," + cx + "," + cz)) {
-            return;
-        }
-
-        UUID key = world.getUID();
-
-        if (!oldData.containsKey(key)) {
-            oldData.put(key, (new File(world.getWorldFolder(), "mcmmo_data")).exists());
-        }
-        else if (oldData.get(key)) {
-            if (convertChunk(new File(world.getWorldFolder(), "mcmmo_data"), cx, cz, world, true)) {
-                return;
-            }
-        }
-
-        ChunkStore chunkStore = null;
-
-        try {
-            chunkStore = readChunkStore(world, cx, cz);
-        }
-        catch (Exception e) { e.printStackTrace(); }
-
-        if (chunkStore == null) {
-            return;
-        }
-
-        store.put(world.getName() + "," + cx + "," + cz, chunkStore);
-    }
-
-    @Override
-    public synchronized void unloadChunk(int cx, int cz, World world) {
-        saveChunk(cx, cz, world);
-
-        if (store.containsKey(world.getName() + "," + cx + "," + cz)) {
-            store.remove(world.getName() + "," + cx + "," + cz);
-
-            //closeChunkStore(world, cx, cz);
-        }
-    }
-
-    @Override
-    public synchronized void saveChunk(int cx, int cz, World world) {
-        if (world == null) {
-            return;
-        }
-
-        String key = world.getName() + "," + cx + "," + cz;
-
-        if (store.containsKey(key)) {
-            ChunkStore out = store.get(world.getName() + "," + cx + "," + cz);
-
-            if (!out.isDirty()) {
-                return;
-            }
-
-            writeChunkStore(world, cx, cz, out);
-        }
-    }
-
-    @Override
-    public synchronized boolean isChunkLoaded(int cx, int cz, World world) {
-        if (world == null) {
-            return false;
-        }
-
-        return store.containsKey(world.getName() + "," + cx + "," + cz);
-    }
-
-    @Override
-    public synchronized void chunkLoaded(int cx, int cz, World world) {}
-
-    @Override
-    public synchronized void chunkUnloaded(int cx, int cz, World world) {
-        if (world == null) {
-            return;
-        }
-
-        unloadChunk(cx, cz, world);
-    }
-
-    @Override
-    public synchronized void saveWorld(World world) {
-        if (world == null) {
-            return;
-        }
-
-        closeAll();
-        String worldName = world.getName();
-
-        List<String> keys = new ArrayList<>(store.keySet());
-        for (String key : keys) {
-            String[] info = key.split(",");
-            if (worldName.equals(info[0])) {
-                try {
-                    saveChunk(Integer.parseInt(info[1]), Integer.parseInt(info[2]), world);
-                }
-                catch (Exception e) {
-                    // Ignore
-                }
-            }
-        }
-    }
-
-    @Override
-    public synchronized void unloadWorld(World world) {
-        if (world == null) {
-            return;
-        }
-
-        String worldName = world.getName();
-
-        List<String> keys = new ArrayList<>(store.keySet());
-        for (String key : keys) {
-            String[] info = key.split(",");
-            if (worldName.equals(info[0])) {
-                try {
-                    unloadChunk(Integer.parseInt(info[1]), Integer.parseInt(info[2]), world);
-                }
-                catch (Exception e) {
-                    // Ignore
-                }
-            }
-        }
-        closeAll();
-    }
-
-    @Override
-    public synchronized void loadWorld(World world) {}
-
-    @Override
-    public synchronized void saveAll() {
-        closeAll();
-
-        for (World world : mcMMO.p.getServer().getWorlds()) {
-            saveWorld(world);
-        }
-    }
-
-    @Override
-    public synchronized void unloadAll() {
-        closeAll();
-
-        for (World world : mcMMO.p.getServer().getWorlds()) {
-            unloadWorld(world);
-        }
-    }
-
-    @Override
-    public synchronized boolean isTrue(int x, int y, int z, World world) {
-        if (world == null) {
-            return false;
-        }
-
-        int cx = x >> 4;
-        int cz = z >> 4;
-
-        String key = world.getName() + "," + cx + "," + cz;
-
-        if (!store.containsKey(key)) {
-            loadChunk(cx, cz, world, null);
-        }
-
-        if (!store.containsKey(key)) {
-            return false;
-        }
-
-        ChunkStore check = store.get(key);
-        int ix = Math.abs(x) % 16;
-        int iz = Math.abs(z) % 16;
-
-        return check.isTrue(ix, y, iz);
-    }
-
-    @Override
-    public synchronized boolean isTrue(Block block) {
-        if (block == null) {
-            return false;
-        }
-
-        return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
-    }
-
-    @Override
-    public synchronized boolean isTrue(BlockState blockState) {
-        if (blockState == null) {
-            return false;
-        }
-
-        return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
-    }
-
-    @Override
-    public synchronized void setTrue(int x, int y, int z, World world) {
-        if (world == null) {
-            return;
-        }
-
-        int cx = x >> 4;
-        int cz = z >> 4;
-
-        int ix = Math.abs(x) % 16;
-        int iz = Math.abs(z) % 16;
-
-        String key = world.getName() + "," + cx + "," + cz;
-
-        if (!store.containsKey(key)) {
-            loadChunk(cx, cz, world, null);
-        }
-
-        ChunkStore cStore = store.get(key);
-
-        if (cStore == null) {
-            cStore = ChunkStoreFactory.getChunkStore(world, cx, cz);
-            store.put(key, cStore);
-        }
-
-        cStore.setTrue(ix, y, iz);
-    }
-
-    @Override
-    public synchronized void setTrue(Block block) {
-        if (block == null) {
-            return;
-        }
-
-        setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
-    }
-
-    @Override
-    public void setTrue(BlockState blockState) {
-        if (blockState == null) {
-            return;
-        }
-
-        setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
-    }
-
-    @Override
-    public synchronized void setFalse(int x, int y, int z, World world) {
-        if (world == null) {
-            return;
-        }
-
-        int cx = x >> 4;
-        int cz = z >> 4;
-
-        int ix = Math.abs(x) % 16;
-        int iz = Math.abs(z) % 16;
-
-        String key = world.getName() + "," + cx + "," + cz;
-
-        if (!store.containsKey(key)) {
-            loadChunk(cx, cz, world, null);
-        }
-
-        ChunkStore cStore = store.get(key);
-
-        if (cStore == null) {
-            return; // No need to make a store for something we will be setting to false
-        }
-
-        cStore.setFalse(ix, y, iz);
-    }
-
-    @Override
-    public synchronized void setFalse(Block block) {
-        if (block == null) {
-            return;
-        }
-
-        setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
-    }
-
-    @Override
-    public synchronized void setFalse(BlockState blockState) {
-        if (blockState == null) {
-            return;
-        }
-
-        setFalse(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
-    }
-
-    @Override
-    public synchronized void cleanUp() {}
-
-    public synchronized void convertChunk(File dataDir, int cx, int cz, World world) {
-        convertChunk(dataDir, cx, cz, world, false);
-    }
-
-    public synchronized boolean convertChunk(File dataDir, int cx, int cz, World world, boolean actually) {
-        if (!actually || !dataDir.exists()) {
-            return false;
-        }
-
-        File cxDir = new File(dataDir, "" + cx);
-        if (!cxDir.exists()) {
-            return false;
-        }
-
-        File czDir = new File(cxDir, "" + cz);
-        if (!czDir.exists()) {
-            return false;
-        }
-
-        boolean conversionSet = false;
-
-        for (BlockStoreConversionZDirectory converter : this.converters) {
-            if (converter == null) {
-                continue;
-            }
-
-            if (converter.taskID >= 0) {
-                continue;
-            }
-
-            converter.start(world, cxDir, czDir);
-            conversionSet = true;
-            break;
-        }
-
-        if (!conversionSet) {
-            BlockStoreConversionZDirectory converter = new BlockStoreConversionZDirectory();
-            converter.start(world, cxDir, czDir);
-            converters.add(converter);
-        }
-
-        return true;
-    }
-}

+ 0 - 39
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java

@@ -1,39 +0,0 @@
-/*
- * This file is part of SpoutPlugin.
- *
- * Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
- * SpoutPlugin is licensed under the GNU Lesser General Public License.
- *
- * SpoutPlugin is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * SpoutPlugin is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-public class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
-    final McMMOSimpleRegionFile rf;
-    final int index;
-
-    McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
-        super(1024);
-        this.rf = rf;
-        this.index = index;
-    }
-
-    @Override
-    public void close() throws IOException {
-        rf.write(index, buf, count);
-    }
-}

+ 0 - 306
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java

@@ -1,306 +0,0 @@
-/*
- * This file is part of SpoutPlugin.
- *
- * Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
- * SpoutPlugin is licensed under the GNU Lesser General Public License.
- *
- * SpoutPlugin is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * SpoutPlugin is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import java.io.*;
-import java.util.ArrayList;
-import java.util.zip.DeflaterOutputStream;
-import java.util.zip.InflaterInputStream;
-
-public class McMMOSimpleRegionFile {
-    private RandomAccessFile file;
-    private final int[] dataStart = new int[1024];
-    private final int[] dataActualLength = new int[1024];
-    private final int[] dataLength = new int[1024];
-    private final ArrayList<Boolean> inuse = new ArrayList<>();
-    private int segmentSize;
-    private int segmentMask;
-    private final int rx;
-    private final int rz;
-    private final int defaultSegmentSize;
-    private final File parent;
-    @SuppressWarnings("unused")
-    private long lastAccessTime = System.currentTimeMillis();
-    @SuppressWarnings("unused")
-    private static final long TIMEOUT_TIME = 300000; // 5 min
-
-    public McMMOSimpleRegionFile(File f, int rx, int rz) {
-        this(f, rx, rz, 10);
-    }
-
-    public McMMOSimpleRegionFile(File f, int rx, int rz, int defaultSegmentSize) {
-        this.rx = rx;
-        this.rz = rz;
-        this.defaultSegmentSize = defaultSegmentSize;
-        this.parent = f;
-
-        lastAccessTime = System.currentTimeMillis();
-        if (file == null) {
-            try {
-                this.file = new RandomAccessFile(parent, "rw");
-
-                if (file.length() < 4096 * 3) {
-                    for (int i = 0; i < 1024 * 3; i++) {
-                        file.writeInt(0);
-                    }
-                    file.seek(4096 * 2);
-                    file.writeInt(defaultSegmentSize);
-                }
-
-                file.seek(4096 * 2);
-
-                this.segmentSize = file.readInt();
-                this.segmentMask = (1 << segmentSize) - 1;
-
-                int reservedSegments = this.sizeToSegments(4096 * 3);
-
-                for (int i = 0; i < reservedSegments; i++) {
-                    while (inuse.size() <= i) {
-                        inuse.add(false);
-                    }
-                    inuse.set(i, true);
-                }
-
-                file.seek(0);
-
-                for (int i = 0; i < 1024; i++) {
-                    dataStart[i] = file.readInt();
-                }
-
-                for (int i = 0; i < 1024; i++) {
-                    dataActualLength[i] = file.readInt();
-                    dataLength[i] = sizeToSegments(dataActualLength[i]);
-                    setInUse(i, true);
-                }
-
-                extendFile();
-            }
-            catch (IOException fnfe) {
-                throw new RuntimeException(fnfe);
-            }
-        }
-    }
-
-    public synchronized final RandomAccessFile getFile() {
-        lastAccessTime = System.currentTimeMillis();
-        if (file == null) {
-            try {
-                this.file = new RandomAccessFile(parent, "rw");
-
-                if (file.length() < 4096 * 3) {
-                    for (int i = 0; i < 1024 * 3; i++) {
-                        file.writeInt(0);
-                    }
-                    file.seek(4096 * 2);
-                    file.writeInt(defaultSegmentSize);
-                }
-
-                file.seek(4096 * 2);
-
-                this.segmentSize = file.readInt();
-                this.segmentMask = (1 << segmentSize) - 1;
-
-                int reservedSegments = this.sizeToSegments(4096 * 3);
-
-                for (int i = 0; i < reservedSegments; i++) {
-                    while (inuse.size() <= i) {
-                        inuse.add(false);
-                    }
-                    inuse.set(i, true);
-                }
-
-                file.seek(0);
-
-                for (int i = 0; i < 1024; i++) {
-                    dataStart[i] = file.readInt();
-                }
-
-                for (int i = 0; i < 1024; i++) {
-                    dataActualLength[i] = file.readInt();
-                    dataLength[i] = sizeToSegments(dataActualLength[i]);
-                    setInUse(i, true);
-                }
-
-                extendFile();
-            }
-            catch (IOException fnfe) {
-                throw new RuntimeException(fnfe);
-            }
-        }
-        return file;
-    }
-
-    public synchronized boolean testCloseTimeout() {
-        /*
-        if (System.currentTimeMillis() - TIMEOUT_TIME > lastAccessTime) {
-            close();
-            return true;
-        }
-         */
-        return false;
-    }
-
-    public synchronized DataOutputStream getOutputStream(int x, int z) {
-        int index = getChunkIndex(x, z);
-        return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
-    }
-
-    public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
-        int index = getChunkIndex(x, z);
-        int actualLength = dataActualLength[index];
-
-        if (actualLength == 0) {
-            return null;
-        }
-
-        byte[] data = new byte[actualLength];
-
-        getFile().seek(dataStart[index] << segmentSize);
-        getFile().readFully(data);
-        return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
-    }
-
-    synchronized void write(int index, byte[] buffer, int size) throws IOException {
-        int oldStart = setInUse(index, false);
-        int start = findSpace(oldStart, size);
-        getFile().seek(start << segmentSize);
-        getFile().write(buffer, 0, size);
-        dataStart[index] = start;
-        dataActualLength[index] = size;
-        dataLength[index] = sizeToSegments(size);
-        setInUse(index, true);
-        saveFAT();
-    }
-
-    public synchronized void close() {
-        try {
-            if (file != null) {
-                file.seek(4096 * 2);
-                file.close();
-            }
-
-            file = null;
-        }
-        catch (IOException ioe) {
-            throw new RuntimeException("Unable to close file", ioe);
-        }
-    }
-
-    private synchronized int setInUse(int index, boolean used) {
-        if (dataActualLength[index] == 0) {
-            return dataStart[index];
-        }
-
-        int start = dataStart[index];
-        int end = start + dataLength[index];
-
-        for (int i = start; i < end; i++) {
-            while (i > inuse.size() - 1) {
-                inuse.add(false);
-            }
-
-            Boolean old = inuse.set(i, used);
-            if (old != null && old == used) {
-                if (old) {
-                    throw new IllegalStateException("Attempting to overwrite an in-use segment");
-                }
-
-                throw new IllegalStateException("Attempting to delete empty segment");
-            }
-        }
-
-        return dataStart[index];
-    }
-
-    private synchronized void extendFile() throws IOException {
-        long extend = (-getFile().length()) & segmentMask;
-
-        getFile().seek(getFile().length());
-
-        while ((extend--) > 0) {
-            getFile().write(0);
-        }
-    }
-
-    private synchronized int findSpace(int oldStart, int size) {
-        int segments = sizeToSegments(size);
-
-        boolean oldFree = true;
-        for (int i = oldStart; i < inuse.size() && i < oldStart + segments; i++) {
-            if (inuse.get(i)) {
-                oldFree = false;
-                break;
-            }
-        }
-
-        if (oldFree) {
-            return oldStart;
-        }
-
-        int start = 0;
-        int end = 0;
-
-        while (end < inuse.size()) {
-            if (inuse.get(end)) {
-                end++;
-                start = end;
-            }
-            else {
-                end++;
-            }
-
-            if (end - start >= segments) {
-                return start;
-            }
-        }
-
-        return start;
-    }
-
-    private synchronized int sizeToSegments(int size) {
-        if (size <= 0) {
-            return 1;
-        }
-
-        return ((size - 1) >> segmentSize) + 1;
-    }
-
-    private synchronized Integer getChunkIndex(int x, int z) {
-        if (rx != (x >> 5) || rz != (z >> 5)) {
-            throw new RuntimeException(x + ", " + z + " not in region " + rx + ", " + rz);
-        }
-
-        x = x & 0x1F;
-        z = z & 0x1F;
-
-        return (x << 5) + z;
-    }
-
-    private synchronized void saveFAT() throws IOException {
-        getFile().seek(0);
-        for (int i = 0; i < 1024; i++) {
-            getFile().writeInt(dataStart[i]);
-        }
-
-        for (int i = 0; i < 1024; i++) {
-            getFile().writeInt(dataActualLength[i]);
-        }
-    }
-}

+ 0 - 147
src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java

@@ -1,147 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.chunkmeta;
-
-import com.gmail.nossr50.util.blockmeta.ChunkletStore;
-import org.bukkit.Bukkit;
-import org.bukkit.World;
-
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.UUID;
-
-public class PrimitiveChunkStore implements ChunkStore {
-    private static final long serialVersionUID = -1L;
-    transient private boolean dirty = false;
-    /** X, Z, Y */
-    public boolean[][][] store;
-    private static final int CURRENT_VERSION = 7;
-    private static final int MAGIC_NUMBER = 0xEA5EDEBB;
-    private int cx;
-    private int cz;
-    private UUID worldUid;
-
-    public PrimitiveChunkStore(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 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 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;
-    }
-
-    @Override
-    public void copyFrom(ChunkletStore otherStore) {
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < store[0][0].length; y++) {
-                    store[x][z][y] = otherStore.isTrue(x, y, z);
-                }
-            }
-        }
-        dirty = true;
-    }
-
-    private void writeObject(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(ObjectInputStream in) throws IOException, ClassNotFoundException {
-        int magic = in.readInt();
-        // Can be used to determine the format of the file
-        int fileVersionNumber = in.readInt();
-
-        if (magic != MAGIC_NUMBER) {
-            fileVersionNumber = 0;
-        }
-
-        long lsb = in.readLong();
-        long msb = in.readLong();
-        worldUid = new UUID(msb, lsb);
-        cx = in.readInt();
-        cz = in.readInt();
-
-        store = (boolean[][][]) in.readObject();
-
-        if (fileVersionNumber < 5) {
-            fixArray();
-            dirty = true;
-        }
-    }
-
-    private void fixArray() {
-        boolean[][][] temp = this.store;
-        this.store = new boolean[16][16][Bukkit.getWorld(worldUid).getMaxHeight()];
-        for (int x = 0; x < 16; x++) {
-            for (int z = 0; z < 16; z++) {
-                for (int y = 0; y < store[0][0].length; y++) {
-                    try {
-                        store[x][z][y] = temp[x][y][z];
-                    }
-                    catch (Exception e) { e.printStackTrace(); }
-                }
-            }
-        }
-    }
-}

+ 0 - 90
src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java

@@ -1,90 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.conversion;
-
-import com.gmail.nossr50.config.HiddenConfig;
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.scheduler.BukkitScheduler;
-
-import java.io.File;
-
-public class BlockStoreConversionMain implements Runnable {
-    private int taskID, i;
-    private org.bukkit.World world;
-    BukkitScheduler scheduler;
-    File dataDir;
-    File[] xDirs;
-    BlockStoreConversionXDirectory[] converters;
-
-    public BlockStoreConversionMain(org.bukkit.World world) {
-        this.taskID = -1;
-        this.world = world;
-        this.scheduler = mcMMO.p.getServer().getScheduler();
-        this.dataDir = new File(this.world.getWorldFolder(), "mcmmo_data");
-        this.converters = new BlockStoreConversionXDirectory[HiddenConfig.getInstance().getConversionRate()];
-    }
-
-    public void start() {
-        if (this.taskID >= 0) {
-            return;
-        }
-
-        this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
-    }
-
-    @Override
-    public void run() {
-        if (!this.dataDir.exists()) {
-            softStop();
-            return;
-        }
-
-        if (!this.dataDir.isDirectory()) {
-            this.dataDir.delete();
-            softStop();
-            return;
-        }
-
-        if (this.dataDir.listFiles().length <= 0) {
-            this.dataDir.delete();
-            softStop();
-            return;
-        }
-
-        this.xDirs = this.dataDir.listFiles();
-
-        for (this.i = 0; (this.i < HiddenConfig.getInstance().getConversionRate()) && (this.i < this.xDirs.length); this.i++) {
-            if (this.converters[this.i] == null) {
-                this.converters[this.i] = new BlockStoreConversionXDirectory();
-            }
-
-            this.converters[this.i].start(this.world, this.xDirs[this.i]);
-        }
-
-        softStop();
-    }
-
-    public void stop() {
-        if (this.taskID < 0) {
-            return;
-        }
-
-        this.scheduler.cancelTask(this.taskID);
-        this.taskID = -1;
-    }
-
-    public void softStop() {
-        stop();
-
-        if (this.dataDir.exists() && this.dataDir.isDirectory()) {
-            start();
-            return;
-        }
-
-        mcMMO.p.getLogger().info("Finished converting the storage for " + world.getName() + ".");
-
-        this.dataDir = null;
-        this.xDirs = null;
-        this.world = null;
-        this.scheduler = null;
-        this.converters = null;
-    }
-}

+ 0 - 80
src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java

@@ -1,80 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.conversion;
-
-import com.gmail.nossr50.config.HiddenConfig;
-import com.gmail.nossr50.mcMMO;
-import org.bukkit.scheduler.BukkitScheduler;
-
-import java.io.File;
-
-public class BlockStoreConversionXDirectory implements Runnable {
-    private int taskID, i;
-    private org.bukkit.World world;
-    BukkitScheduler scheduler;
-    File dataDir;
-    File[] zDirs;
-    BlockStoreConversionZDirectory[] converters;
-
-    public BlockStoreConversionXDirectory() {
-        this.taskID = -1;
-    }
-
-    public void start(org.bukkit.World world, File dataDir) {
-        this.world = world;
-        this.scheduler = mcMMO.p.getServer().getScheduler();
-        this.converters = new BlockStoreConversionZDirectory[HiddenConfig.getInstance().getConversionRate()];
-        this.dataDir = dataDir;
-
-        if (this.taskID >= 0) {
-            return;
-        }
-
-        this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
-    }
-
-    @Override
-    public void run() {
-        if (!this.dataDir.exists()) {
-            stop();
-            return;
-        }
-
-        if (!this.dataDir.isDirectory()) {
-            this.dataDir.delete();
-            stop();
-            return;
-        }
-
-        if (this.dataDir.listFiles().length <= 0) {
-            this.dataDir.delete();
-            stop();
-            return;
-        }
-
-        this.zDirs = this.dataDir.listFiles();
-
-        for (this.i = 0; (this.i < HiddenConfig.getInstance().getConversionRate()) && (this.i < this.zDirs.length); this.i++) {
-            if (this.converters[this.i] == null) {
-                this.converters[this.i] = new BlockStoreConversionZDirectory();
-            }
-
-            this.converters[this.i].start(this.world, this.dataDir, this.zDirs[this.i]);
-        }
-
-        stop();
-    }
-
-    public void stop() {
-        if (this.taskID < 0) {
-            return;
-        }
-
-        this.scheduler.cancelTask(this.taskID);
-        this.taskID = -1;
-
-        this.dataDir = null;
-        this.zDirs = null;
-        this.world = null;
-        this.scheduler = null;
-        this.converters = null;
-    }
-}

+ 0 - 191
src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java

@@ -1,191 +0,0 @@
-package com.gmail.nossr50.util.blockmeta.conversion;
-
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.blockmeta.ChunkletStore;
-import com.gmail.nossr50.util.blockmeta.HashChunkletManager;
-import com.gmail.nossr50.util.blockmeta.PrimitiveChunkletStore;
-import com.gmail.nossr50.util.blockmeta.PrimitiveExChunkletStore;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.HashChunkManager;
-import com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore;
-import org.bukkit.scheduler.BukkitScheduler;
-
-import java.io.File;
-
-public class BlockStoreConversionZDirectory implements Runnable {
-    public int taskID, cx, cz, x, y, z, y2, xPos, zPos, cxPos, czPos;
-    private String cxs, czs, chunkletName, chunkName;
-    private org.bukkit.World world;
-    private BukkitScheduler scheduler;
-    private File xDir, dataDir;
-    private HashChunkletManager manager;
-    private HashChunkManager newManager;
-    private ChunkletStore tempChunklet;
-    private PrimitiveChunkletStore primitiveChunklet = null;
-    private PrimitiveExChunkletStore primitiveExChunklet = null;
-    private PrimitiveChunkStore currentChunk;
-    private boolean[] oldArray, newArray;
-
-    public BlockStoreConversionZDirectory() {
-        this.taskID = -1;
-    }
-
-    public void start(org.bukkit.World world, File xDir, File dataDir) {
-        this.world = world;
-        this.scheduler = mcMMO.p.getServer().getScheduler();
-        this.manager = new HashChunkletManager();
-        this.newManager = (HashChunkManager) mcMMO.getPlaceStore();
-        this.dataDir = dataDir;
-        this.xDir = xDir;
-
-        if (this.taskID >= 0) {
-            return;
-        }
-
-        this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
-    }
-
-    @Override
-    public void run() {
-        if (!this.dataDir.exists()) {
-            stop();
-            return;
-        }
-
-        if (!this.dataDir.isDirectory()) {
-            this.dataDir.delete();
-            stop();
-            return;
-        }
-
-        if (this.dataDir.listFiles().length <= 0) {
-            this.dataDir.delete();
-            stop();
-            return;
-        }
-
-        this.cxs = this.xDir.getName();
-        this.czs = this.dataDir.getName();
-        this.cx = 0;
-        this.cz = 0;
-
-        try {
-            this.cx = Integer.parseInt(this.cxs);
-            this.cz = Integer.parseInt(this.czs);
-        }
-        catch (Exception e) {
-            this.dataDir.delete();
-            stop();
-            return;
-        }
-
-        this.manager.loadChunk(this.cx, this.cz, this.world);
-
-        for (this.y = 0; this.y < (this.world.getMaxHeight() / 64); this.y++) {
-            this.chunkletName = this.world.getName() + "," + this.cx + "," + this.cz + "," + this.y;
-            this.tempChunklet = this.manager.store.get(this.chunkletName);
-
-            if (this.tempChunklet instanceof PrimitiveChunkletStore) {
-                this.primitiveChunklet = (PrimitiveChunkletStore) this.tempChunklet;
-            }
-            else if (this.tempChunklet instanceof PrimitiveExChunkletStore) {
-                this.primitiveExChunklet = (PrimitiveExChunkletStore) this.tempChunklet;
-            }
-
-            if (this.tempChunklet == null) {
-                continue;
-            }
-
-            this.chunkName = this.world.getName() + "," + this.cx + "," + this.cz;
-            this.currentChunk = (PrimitiveChunkStore) this.newManager.store.get(this.chunkName);
-
-            if (this.currentChunk != null) {
-                this.xPos = this.cx * 16;
-                this.zPos = this.cz * 16;
-
-                for (this.x = 0; this.x < 16; this.x++) {
-                    for (this.z = 0; this.z < 16; this.z++) {
-                        this.cxPos = this.xPos + this.x;
-                        this.czPos = this.zPos + this.z;
-
-                        for (this.y2 = (64 * this.y); this.y2 < (64 * this.y + 64); this.y2++) {
-                            try {
-                                if (!this.manager.isTrue(this.cxPos, this.y2, this.czPos, this.world)) {
-                                    continue;
-                                }
-
-                                this.newManager.setTrue(this.cxPos, this.y2, this.czPos, this.world);
-                            }
-                            catch (Exception e) { e.printStackTrace(); }
-                        }
-                    }
-                }
-
-                continue;
-            }
-
-            this.newManager.setTrue(this.cx * 16, 0, this.cz * 16, this.world);
-            this.newManager.setFalse(this.cx * 16, 0, this.cz * 16, this.world);
-            this.currentChunk = (PrimitiveChunkStore) this.newManager.store.get(this.chunkName);
-
-            for (this.x = 0; this.x < 16; this.x++) {
-                for (this.z = 0; this.z < 16; this.z++) {
-                    if (this.primitiveChunklet != null) {
-                        this.oldArray = this.primitiveChunklet.store[x][z];
-                    }
-
-                    if (this.primitiveExChunklet != null) {
-                        this.oldArray = this.primitiveExChunklet.store[x][z];
-                    }
-                    else {
-                        return;
-                    }
-
-                    this.newArray = this.currentChunk.store[x][z];
-
-                    if (this.oldArray.length < 64) {
-                        return;
-                    }
-                    else if (this.newArray.length < ((this.y * 64) + 64)) {
-                        return;
-                    }
-
-                    System.arraycopy(this.oldArray, 0, this.newArray, (this.y * 64), 64);
-                }
-            }
-        }
-
-        this.manager.unloadChunk(this.cx, this.cz, this.world);
-        this.newManager.unloadChunk(this.cx, this.cz, this.world);
-
-        for (File yFile : dataDir.listFiles()) {
-            if (!yFile.exists()) {
-                continue;
-            }
-
-            yFile.delete();
-        }
-
-        stop();
-    }
-
-    public void stop() {
-        if (this.taskID < 0) {
-            return;
-        }
-
-        this.scheduler.cancelTask(taskID);
-        this.taskID = -1;
-
-        this.cxs = null;
-        this.czs = null;
-        this.chunkletName = null;
-        this.chunkName = null;
-        this.manager = null;
-        this.xDir = null;
-        this.dataDir = null;
-        this.tempChunklet = null;
-        this.primitiveChunklet = null;
-        this.primitiveExChunklet = null;
-        this.currentChunk = null;
-    }
-}

+ 308 - 0
src/test/java/ChunkStoreTest.java

@@ -0,0 +1,308 @@
+import com.gmail.nossr50.util.blockmeta.*;
+import com.google.common.io.Files;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.junit.*;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.*;
+import java.util.UUID;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * Could be alot better.  But some tests are better than none!  Tests the major things, still kinda unit-testy.  Verifies that the serialization isn't completely broken.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(Bukkit.class)
+public class ChunkStoreTest {
+    private static File tempDir;
+    @BeforeClass
+    public static void setUpClass() {
+        tempDir = Files.createTempDir();
+    }
+
+    @AfterClass
+    public static void tearDownClass() {
+        recursiveDelete(tempDir);
+    }
+
+    private World mockWorld;
+    @Before
+    public void setUpMock(){
+        UUID worldUUID = UUID.randomUUID();
+        mockWorld = mock(World.class);
+        Mockito.when(mockWorld.getUID()).thenReturn(worldUUID);
+        Mockito.when(mockWorld.getMaxHeight()).thenReturn(256);
+        Mockito.when(mockWorld.getWorldFolder()).thenReturn(tempDir);
+        PowerMockito.mockStatic(Bukkit.class);
+        Mockito.when(Bukkit.getWorld(worldUUID)).thenReturn(mockWorld);
+    }
+
+    @Test
+    public void testSetValue() {
+        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
+        original.setTrue(0, 0, 0);
+        Assert.assertTrue(original.isTrue(0, 0, 0));
+        original.setFalse(0, 0, 0);
+        Assert.assertFalse(original.isTrue(0, 0, 0));
+    }
+
+    @Test
+    public void testIsEmpty() {
+        BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
+        Assert.assertTrue(original.isEmpty());
+        original.setTrue(0, 0, 0);
+        original.setFalse(0, 0, 0);
+        Assert.assertTrue(original.isEmpty());
+    }
+
+    @Test
+    public 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
+    public 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
+    public 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);
+        ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
+        assertEqual(original, deserialized);
+    }
+
+    @Test
+    public void testSimpleRegionRoundtrip() throws IOException {
+        LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 12);
+        original.setTrue(14, 89, 12);
+        original.setTrue(14, 90, 12);
+        original.setTrue(13, 89, 12);
+        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));
+        }
+        region.close();
+        region = new McMMOSimpleRegionFile(file, 0, 0);
+        try (DataInputStream is = region.getInputStream(original.getChunkX(), original.getChunkZ()))
+        {
+            Assert.assertNotNull(is);
+            ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(is);
+            assertEqual(original, deserialized);
+        }
+        region.close();
+        file.delete();
+    }
+
+    @Test
+    public void testSimpleRegionRejectsOutOfBounds() {
+        File file = new File(tempDir, "SimpleRegionRoundTrip.region");
+        McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
+        assertThrows(() -> region.getOutputStream(-1, 0), IndexOutOfBoundsException.class);
+        assertThrows(() -> region.getOutputStream(0, -1), IndexOutOfBoundsException.class);
+        assertThrows(() -> region.getOutputStream(32, 0), IndexOutOfBoundsException.class);
+        assertThrows(() -> region.getOutputStream(0, 32), IndexOutOfBoundsException.class);
+        region.close();
+    }
+
+    @Test
+    public void testChunkStoreRejectsOutOfBounds() {
+        ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
+        assertThrows(() -> chunkStore.setTrue(-1, 0, 0), IndexOutOfBoundsException.class);
+        assertThrows(() -> chunkStore.setTrue(0, -1, 0), IndexOutOfBoundsException.class);
+        assertThrows(() -> chunkStore.setTrue(0, 0, -1), IndexOutOfBoundsException.class);
+        assertThrows(() -> chunkStore.setTrue(16, 0, 0), IndexOutOfBoundsException.class);
+        assertThrows(() -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0), IndexOutOfBoundsException.class);
+        assertThrows(() -> chunkStore.setTrue(0, 0, 16), IndexOutOfBoundsException.class);
+    }
+
+    @Test
+    public void testRegressionChunkMirrorBug() {
+        ChunkManager chunkManager = new HashChunkManager();
+        chunkManager.setTrue(15,0,15, mockWorld);
+        chunkManager.setFalse(-15, 0, -15, mockWorld);
+        Assert.assertTrue(chunkManager.isTrue(15, 0, 15, mockWorld));
+    }
+
+    private interface Delegate {
+        void run();
+    }
+
+    private void assertThrows(Delegate delegate, Class<?> clazz) {
+        try {
+            delegate.run();
+            Assert.fail(); // We didn't throw
+        }
+        catch (Throwable t) {
+            Assert.assertTrue(t.getClass().equals(clazz));
+        }
+    }
+
+    private void assertEqual(ChunkStore expected, ChunkStore actual)
+    {
+        Assert.assertEquals(expected.getChunkX(), actual.getChunkX());
+        Assert.assertEquals(expected.getChunkZ(), actual.getChunkZ());
+        Assert.assertEquals(expected.getWorldId(), actual.getWorldId());
+        for (int y = 0; y < 256; y++)
+            for (int x = 0; x < 16; x++)
+                for (int z = 0; z < 16; z++)
+                    Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
+    }
+
+    private static void recursiveDelete(File directoryToBeDeleted) {
+        if (directoryToBeDeleted.isDirectory()) {
+            for (File file : directoryToBeDeleted.listFiles()) {
+                recursiveDelete(file);
+            }
+        }
+        directoryToBeDeleted.delete();
+    }
+
+    private static byte[] serializeChunkstore(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 int cx;
+        private int cz;
+        private UUID worldUid;
+
+        public LegacyChunkStore(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 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(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(ObjectInputStream in) throws IOException, ClassNotFoundException {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static class UnitTestObjectOutputStream extends ObjectOutputStream {
+        public UnitTestObjectOutputStream(OutputStream outputStream) throws IOException {
+            super(outputStream);
+        }
+
+        @Override
+        public void writeUTF(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);
+        }
+    }
+}