diff --git a/Changelog.txt b/Changelog.txt
index 99717caaf..64cce7473 100644
--- a/Changelog.txt
+++ b/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
diff --git a/pom.xml b/pom.xml
index e353ab913..755d4490f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
com.gmail.nossr50.mcMMO
mcMMO
- 2.1.164
+ 2.1.165-SNAPSHOT
mcMMO
https://github.com/mcMMO-Dev/mcMMO
@@ -279,7 +279,25 @@
junit
junit-dep
- 4.10
+ 4.11
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 2.0.7
+ test
+
+
+ org.powermock
+ powermock-api-mockito2
+ 2.0.7
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.4.6
test
diff --git a/src/main/java/com/gmail/nossr50/listeners/WorldListener.java b/src/main/java/com/gmail/nossr50/listeners/WorldListener.java
index 5fc386c86..650a0e0cc 100644
--- a/src/main/java/com/gmail/nossr50/listeners/WorldListener.java
+++ b/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.
*
diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java
index b98ad9f9f..529f70fb5 100644
--- a/src/main/java/com/gmail/nossr50/mcMMO.java
+++ b/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(); }
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/BitSetChunkStore.java
new file mode 100644
index 000000000..dce7ab174
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
old mode 100755
new mode 100644
similarity index 61%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManager.java
index d64824a0e..5cb50ac7f
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManager.java
+++ b/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
*
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManagerFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
old mode 100755
new mode 100644
similarity index 86%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManagerFactory.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/ChunkManagerFactory.java
index 2b4d90349..a290c5e2a
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkManagerFactory.java
+++ b/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;
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
old mode 100755
new mode 100644
similarity index 79%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/ChunkStore.java
index 69b2acae1..eca783ccd
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStore.java
+++ b/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
*
@@ -64,15 +66,18 @@ public interface ChunkStore extends Serializable {
*/
void setFalse(int x, int y, int z);
+ /**
+ * 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
+ */
+ void set(int x, int y, int z, boolean value);
+
/**
* @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);
}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java
deleted file mode 100755
index feb54acd3..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManager.java
+++ /dev/null
@@ -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();
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java
deleted file mode 100755
index 39f8732d3..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletManagerFactory.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java
deleted file mode 100755
index 9b1537782..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStore.java
+++ /dev/null
@@ -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);
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java
deleted file mode 100755
index 1fb4a315a..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/ChunkletStoreFactory.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkManager.java
new file mode 100644
index 000000000..888937872
--- /dev/null
+++ b/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 regionMap = new HashMap<>(); // Tracks active regions
+ private final HashMap> chunkUsageMap = new HashMap<>(); // Tracks active chunks by region
+ private final HashMap 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 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 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 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() {}
+}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java
deleted file mode 100755
index c2fc23faf..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/HashChunkletManager.java
+++ /dev/null
@@ -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 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;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java b/src/main/java/com/gmail/nossr50/util/blockmeta/McMMOSimpleRegionFile.java
new file mode 100644
index 000000000..bef730ff4
--- /dev/null
+++ b/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
+ * 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 .
+ */
+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
+ }
+}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
old mode 100755
new mode 100644
similarity index 54%
rename from src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java
rename to src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkManager.java
index 3081b0938..b777fa349
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/NullChunkManager.java
+++ b/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;
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java
deleted file mode 100755
index 304ef8780..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/NullChunkletManager.java
+++ /dev/null
@@ -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() {
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java
deleted file mode 100755
index 8dfe3cb8d..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveChunkletStore.java
+++ /dev/null
@@ -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);
- }
- }
- }
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java
deleted file mode 100755
index 187ad0dff..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/PrimitiveExChunkletStore.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java
deleted file mode 100755
index 53528ab66..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/ChunkStoreFactory.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java
deleted file mode 100755
index 05153816f..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/HashChunkManager.java
+++ /dev/null
@@ -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> regionFiles = new HashMap<>();
- public HashMap store = new HashMap<>();
- public ArrayList converters = new ArrayList<>();
- private final HashMap oldData = new HashMap<>();
-
- @Override
- public synchronized void closeAll() {
- for (UUID uid : regionFiles.keySet()) {
- HashMap worldRegions = regionFiles.get(uid);
- for (Iterator 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 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 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 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;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java
deleted file mode 100644
index c2f158b95..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleChunkBuffer.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * This file is part of SpoutPlugin.
- *
- * Copyright (c) 2011-2012, SpoutDev
- * 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 .
- */
-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);
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java
deleted file mode 100644
index 2193417d8..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/McMMOSimpleRegionFile.java
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- * This file is part of SpoutPlugin.
- *
- * Copyright (c) 2011-2012, SpoutDev
- * 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 .
- */
-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 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]);
- }
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java b/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java
deleted file mode 100755
index d1866acab..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/chunkmeta/PrimitiveChunkStore.java
+++ /dev/null
@@ -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(); }
- }
- }
- }
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java b/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java
deleted file mode 100755
index 9dcb20c2a..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionMain.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java
deleted file mode 100755
index a64eec843..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionXDirectory.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java b/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java
deleted file mode 100755
index 4a32a679c..000000000
--- a/src/main/java/com/gmail/nossr50/util/blockmeta/conversion/BlockStoreConversionZDirectory.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/test/java/ChunkStoreTest.java b/src/test/java/ChunkStoreTest.java
new file mode 100644
index 000000000..9d4e4995d
--- /dev/null
+++ b/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);
+ }
+ }
+}