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); + } + } +}